Compare commits

..

341 commits

Author SHA1 Message Date
MrWeez 4bf534ad50
Fix error 500 on the checkout page, small code refactor and total display fix (#963)
* fix: replaced number_format by round because this caused error 500 on checkout

* refactor: code has been made more readable

* fix: incorrect display of total on checkout
2024-05-24 09:38:31 +02:00
S0ly 396d255f6b
fix: product description showing up instead of name in two locations (#957)
made by @kenshin133
2024-05-22 22:13:42 +02:00
kenshin133 da622b82c3
fixed product description showing up instead of name in two locations
This seems to be referenced two places, admin/servers and admin/users/1

I could not find any other effects of this change but do feel free to double check my work.

themes/default/views/admin/servers/table.blade.php
And
themes/default/views/admin/users/show.blade.php
and perhaps
themes/default/views/admin/users/edit.blade.php

have
data: 'resources', name: 'product.name',

In them, another fix would be to alter this to data: 'product.name' but the proposed change seems like the right one.
2024-05-22 10:36:32 -04:00
S0ly 9c44e3885c
ADD: "Buy more" button, if user does not have enough credits (#956)
from @MrWeez
2024-05-22 15:33:17 +02:00
MrWeez 920279cc37 Buy more button, if user does not have enough credits 2024-05-22 13:18:21 +00:00
Dennis 159ba02c84
[Feature] Update and Refactor the Github and Doc (#947) 2024-05-07 10:54:54 +02:00
S0ly 812b817968 ADD: donation to the project main readme 2024-05-07 10:32:24 +02:00
S0ly 6f55e306c1 ADD: warning to default pull request text 2024-05-07 10:22:32 +02:00
S0ly f563dcdd74 FIX: links in contributing.md 2024-05-07 10:19:27 +02:00
S0ly 8c98acb20a FIX: links in main readme 2024-05-07 10:18:22 +02:00
S0ly 5ea4ceda6b UPDATE: made a better main readme 2024-05-07 10:00:00 +02:00
S0ly 14e200f88e UPDATE: made a better pull request template 2024-05-07 09:59:50 +02:00
S0ly cad6361e49 UPDATE: made a better contributing policy 2024-05-07 09:59:39 +02:00
S0ly f12ab58c8e UPDATE: made a better code of conduct 2024-05-07 09:59:27 +02:00
S0ly 44a6946c44 UPDATE: made a better security policy 2024-05-07 09:59:15 +02:00
S0ly 9b45d76b44 ADD: Basic Security Policy 2024-05-06 12:30:52 +02:00
S0ly 0f138cab1d ADD: basic Pull request tempalte 2024-05-06 12:13:35 +02:00
S0ly b2ee49f534 UPDATE: de-mixed the code of conduct from the contribute file 2024-05-06 12:02:18 +02:00
S0ly f18e8c8177 UPDATE: cleared and documented .env.exemple 2024-05-06 11:59:57 +02:00
S0ly 3656c8169d UPDATE: cleared and documented git files 2024-05-06 11:59:44 +02:00
S0ly 077ee2c7e4 REMOVE: strange unused test.json file 2024-05-06 11:54:03 +02:00
S0ly 37e505c37e REMOVE: empty composer file at root 2024-05-06 11:52:52 +02:00
S0ly 971458a01b ADD: empty issue template 2024-04-30 14:46:10 +02:00
S0ly bc190dceb6 FIX: bug and feature displaying differently 2024-04-30 14:41:18 +02:00
S0ly 5c32ff2491 ADD: empty code of conduct in .github 2024-04-30 14:29:18 +02:00
S0ly 75c6dfcb79 UPDATE: moved contributing.md to .github 2024-04-30 14:29:03 +02:00
S0ly d5ffe982d8 UPDATE: moved building.md to a readme in the developement docker 2024-04-30 14:28:48 +02:00
Ferks fa857e8a66
fix: LegacySettingsMigrations (#941) 2024-04-25 09:27:42 -04:00
IceToast e35bbddc40 chore: ♻️ Add encrypted prop back in 2024-04-24 10:54:06 +02:00
IceToast 931f08a1d7 fix: 🐛 Try Catch for upgrade routing 2024-04-24 10:47:39 +02:00
IceToast 38cf81d92d fix: 🐛 Use groups on getNewValue method 2024-04-12 21:01:49 +02:00
IceToast 330ea45c5b fix: 🐛 Try catch for possible non available settings 2024-04-12 21:01:00 +02:00
IceToast 48cce11ebc fix: 🚑️ Use LegacySettingsMigration && mind nullable values 2024-04-12 14:04:32 +02:00
IceToast 7f0d934ed5 feat: Create LegacySettingsMigration to centralize migration methods 2024-04-12 14:03:11 +02:00
1day2die 6a11341c0a Fix roles notification 2024-03-14 14:40:08 +01:00
Dennis 391436c455
Minor changes and additions (#931) 2024-02-29 12:44:15 +01:00
â€MrWeez 6f6517033e Fixed old link and name 2024-02-29 14:15:58 +03:00
â€MrWeez afe3fd23c5 Repair docker image for PHP-FPM by Hyd3r1 2024-02-29 13:42:15 +03:00
â€MrWeez bfb169e191 Added support for php8.3 2024-02-29 01:29:31 +03:00
Dennis 0e29eb171a
fix: installer showing feeding failed, as well as adding proper error handling (#929) 2024-02-28 12:18:32 +01:00
Vikas Dongre 9cc6e83819 fix: installer showing feeding failed, as well as adding proper error handling. 2024-02-17 23:38:30 +05:30
1day2die 6d61ca5cef fix installer 2024-02-05 09:27:24 +01:00
Ferks-FK 3b99ae527f closes #684 2023-10-28 13:03:23 -04:00
Ferks-FK f3a341fa1b Fix role search 2023-10-28 11:59:00 -04:00
Ferks-FK 654932225a Closes #696 2023-10-22 13:48:05 -04:00
Ferks-FK a75863909c Closes #905 2023-10-22 11:24:19 -04:00
Ferks-FK 03f8a7d614 Closes #911 2023-10-22 11:18:50 -04:00
Ferks-FK 754d4f178f Add encrypted migrations and closes #897 2023-10-22 11:11:49 -04:00
Ferks-FK ee5b99ce71 fix: 🐛 Fix the infinite credits exploit in development. 2023-10-01 09:59:32 -04:00
Ferks-FK 22413c3b30 Remove unneeded function 2023-09-17 14:33:21 -04:00
Ferks-FK 04940f040b Aways check if has coupon code in the request. 2023-09-17 14:31:14 -04:00
Ferks-FK a471cb4021 fix: 🐛 fix #901 2023-09-17 13:01:37 -04:00
Ferks-FK 7ecc29487e fix: 🐛 Fix infinite credit exploit when checking email several times. 2023-09-16 13:20:31 -04:00
Dennis f9a102509b
[FIX] Favicon preview, locale setting fix, 500 error on server creation, installer issue(#903) 2023-07-31 15:14:08 +02:00
1day2die 90ebe1468d fix installer mail issue 2023-07-31 15:13:26 +02:00
1day2die 4069b5fd35 500 when servercreation is disabled 2023-07-31 15:08:44 +02:00
1day2die 9ced34a51b available locales is required 2023-07-31 15:01:32 +02:00
1day2die df0b9f57a4 add favicon on preview 2023-07-31 14:57:04 +02:00
IceToast c4b75bf760 fix: 🐛 Settings description 2023-06-25 15:10:47 +02:00
IceToast 8fe6ad0d52 fix: 🐛 Description of settings 2023-06-25 14:30:03 +02:00
Dennis b87f3ce82e
Incorrect output of referral system data (#881) 2023-06-10 20:26:36 +02:00
Dennis 20772eb285
Removing duplicate rows (#878) 2023-06-10 20:26:10 +02:00
MrWeez 2ca38e5906
Incorrect data output 2023-06-10 18:07:52 +03:00
MrWeez 1515615507
Merge branch 'Ctrlpanel-gg:development' into development 2023-06-10 13:28:18 +03:00
IceToast d5e9f3cdaf chore: 🔥 Remove unused extension routes 2023-06-10 02:03:37 +02:00
Dennis 0b1670ad50
Fix payment_method 2023-06-10 01:24:16 +02:00
Dennis dc232d66d4
Change design of Roles on list (#879) 2023-06-09 23:59:25 +02:00
Dennis 3a6362402b
Some smaller fixes (#880) 2023-06-09 23:57:48 +02:00
1day2die 74c7fc9da1 more details on admin overview 2023-06-09 23:55:17 +02:00
1day2die 31bbc4f906 fix wrong credentials message on username 2023-06-09 23:43:06 +02:00
1day2die 77e5b1302e move oom killer 2023-06-09 23:29:29 +02:00
1day2die dfd5c184af increase max width to show lonjg emails on user show 2023-06-09 23:25:58 +02:00
1day2die 9a04f373f0 fix voucher showing expiry "never" 2023-06-09 23:23:23 +02:00
1day2die 3a348ee894 fix installer showing php8.2 as red 2023-06-09 23:21:40 +02:00
1day2die 8785325a94 fix referral not being shown on home 2023-06-09 23:19:12 +02:00
1day2die 458d6a3f26 Change design of Roles on list 2023-06-09 23:13:02 +02:00
MrWeez 57ca712254
Removing duplicate rows 2023-06-09 17:29:25 +03:00
MrWeez fd5790c775 Translation of some fields 2023-06-09 12:48:31 +02:00
MrWeez 3b18b79b47 Language variables 2023-06-09 12:48:31 +02:00
MrWeez 7795ea3d98 Fixing the path of the installation log file 2023-06-09 11:23:32 +02:00
Dennis e7fd842f4b
"No Server" Option (#870) 2023-06-08 23:36:48 +02:00
Dennis 15a8807642
"No Server" Option 2023-06-08 23:36:16 +02:00
MrWeez 61d6240717 Correction of incorrect text output 2023-06-08 22:39:27 +02:00
IceToast eae524b653 chore: 🌐 Change all language labels to english lables 2023-06-08 20:24:15 +02:00
IceToast 21e900040a fix: remove unused "action area" 2023-06-08 20:24:15 +02:00
IceToast af8eb7b1a3 fix: 🐛 fix text typo and add localisation 2023-06-08 20:24:15 +02:00
IceToast 5027986b00 chore: remove old file 2023-06-08 20:24:15 +02:00
IceToast 466f903209 fix: 🥅 Extend error message 2023-06-08 20:24:15 +02:00
IceToast 416e2f6d00 fix: 🐛 Show coupon result with amount 2023-06-08 20:24:15 +02:00
IceToast 2c2279af56 refactor: ♻️ remove jquery code 2023-06-08 20:24:15 +02:00
IceToast d365b122db fix: 🐛 Coupon frontend, calculation and submitting 2023-06-08 20:24:15 +02:00
IceToast 2161464ee3 feat: Added Enum for payment states 2023-06-08 20:24:15 +02:00
IceToast 48e5cae9a1 feat: Migrate other payment gateways to new payment pattern 2023-06-08 20:24:15 +02:00
IceToast 4873992c48 refactor: ️ remove coupon instance from event 2023-06-08 20:24:15 +02:00
IceToast fc84e7d1db feat: Enhance coupon validation and application 2023-06-08 20:24:15 +02:00
IceToast c05b4d8820 feat: Move Payment concerns from extension to payment controller 2023-06-08 20:24:15 +02:00
Ferks-FK 6d50834f9c Just by improving the code. 2023-06-08 20:24:15 +02:00
Ferks-FK 4320fa9ef6 Make it possible to disable coupons 2023-06-08 20:24:15 +02:00
Ferks-FK 24ce267962 Implement coupons for the other gateways. 2023-06-08 20:24:15 +02:00
Ferks-FK bad9d0ec12 It is good to set the application's timezone. 2023-06-08 20:24:15 +02:00
Ferks-FK 640468acbe Finish the coupon base. 2023-06-08 20:24:15 +02:00
Ferks-FK 490e11572d Use casts instead of converting values 2023-06-08 20:24:15 +02:00
Ferks-FK 8f7ad95506 Some improvements, and some not so much... 2023-06-08 20:24:15 +02:00
Ferks-FK 72f3decd3f Feat: Let's implement the coupons now 2023-06-08 20:24:15 +02:00
IceToast e03ac5dae0 fix: 🐛 missed one line for typo fix 2023-06-08 20:23:06 +02:00
IceToast e8a6adb4e6 fix: 🚚 Typo cancelled -> canceled 2023-06-08 16:10:42 +02:00
IceToast 820f47738d fix: 🐛 typo 2023-06-04 16:10:09 +02:00
IceToast 957da3dc78 chore: 🔥 remove error_log 2023-06-04 16:08:47 +02:00
IceToast 83e153058c fix: 🐛locale default language selection 2023-06-04 16:08:22 +02:00
IceToast b97bc8edf7 fix: 🚚 Move and remove some settings 2023-06-04 15:41:51 +02:00
1day2die da29ead3cd fix overview credit erarnings 2023-05-17 09:51:04 +02:00
1day2die 9da53bd02f Monthly calculation on server index 2023-05-17 09:51:04 +02:00
1day2die 33b402bdee add select 2 to roles-permissions 2023-05-17 09:16:20 +02:00
Dennis b330e5a617
Fix Server upgrade || fix server overview total credits 2023-05-16 22:05:51 +02:00
Dennis fec6c198d2
Merge branch 'development' into development 2023-05-16 22:05:34 +02:00
1day2die 680ffefb1e fix server upgrade while installation 2023-05-16 22:03:52 +02:00
1day2die dd962a0afb fix admin overview total credits counter 2023-05-16 21:58:05 +02:00
1day2die fd1ce85723 remove OOM killer from product info 2023-05-16 21:47:34 +02:00
1day2die 2b0321733e fix server upgrade 2023-05-16 21:38:09 +02:00
1day2die 2c9b933c6d fix server upgrade 2023-05-16 21:36:59 +02:00
1day2die f37b78d750 ticket category permissions 2023-05-16 21:33:23 +02:00
1day2die 67cbde2fa1 ticket category permissions 2023-05-16 21:32:04 +02:00
IceToast 21529ddf65 fix: 🐛 Number Input steps 2023-05-12 15:52:58 +02:00
Dennis 0f67661aa7
Extentionsettings, Image uploading 2023-05-11 11:13:22 +02:00
Dennis 7aa20ff30d
Merge branch 'development' into development 2023-05-11 11:12:40 +02:00
1day2die b88727940c clean up 2023-05-11 11:11:22 +02:00
1day2die ce65b9860c Collapseable extension settings 2023-05-11 11:07:09 +02:00
1day2die 1c179702d0 Re-Implement Image uploading 2023-05-11 11:07:09 +02:00
1day2die a1d302701a Collapseable extension settings 2023-05-11 11:06:30 +02:00
1day2die 5cff1e5461 Re-Implement Image uploading 2023-05-11 09:32:13 +02:00
1day2die b58040c457 Fix Notifications 2023-05-09 18:04:40 +02:00
1day2die 38db0a6ee0 Fix Notifications 2023-05-09 18:04:11 +02:00
Dennis 84d8fceafc
Fix Routes, Fix Ptero User ID, Show ReferredBy (#829) 2023-05-08 23:15:50 +02:00
1day2die f53aa0e8b0 Fix Ptero UserID 2023-05-08 23:13:50 +02:00
1day2die 6f74a7379f Show referredBy on User 2023-05-08 23:00:29 +02:00
1day2die 166dccd3de Update web.php 2023-05-08 21:16:06 +02:00
Dennis 259a08b3ba
fix div 2023-05-08 14:48:07 +02:00
Dennis f1d5856bba
Fix Settings bool -> String (#828) 2023-05-08 14:43:21 +02:00
1day2die 9dbd812fc2 Fix Settings bool -> String 2023-05-08 14:42:46 +02:00
Dennis 28e405a55d
Fix some settings (#827) 2023-05-08 14:21:26 +02:00
1day2die f1db71d07f Merge branch 'development' of https://github.com/1day2die/dashboard into development 2023-05-08 14:20:50 +02:00
1day2die 3822c123ba Fix some Settings 2023-05-08 14:20:42 +02:00
Dennis 9ed8f2bf23
ReImplement ThemeSwitcher (#826) 2023-05-08 14:01:39 +02:00
1day2die 6894e2db84 ReImplement ThemeSwitcher 2023-05-08 14:01:02 +02:00
Dennis b4b61bcc47
Fix Ticketmigration (#825) 2023-05-08 13:53:52 +02:00
1day2die 29b5b39d17 fix general settings 2023-05-08 13:53:21 +02:00
1day2die 78dfe881d2 Fix migration 2023-05-08 12:35:52 +02:00
1day2die 98a894fd1e possible fix? 2023-05-08 12:26:49 +02:00
IceToast 88ae8774e0 feat: Add status to payments table 2023-05-08 12:12:16 +02:00
Dennis 06d202f149
ReadME (#824) 2023-05-08 12:09:33 +02:00
1day2die 9804149beb oom killer frontend 2023-05-08 12:07:23 +02:00
Dennis fb6b2ba2ef
Update README.md 2023-05-08 12:05:23 +02:00
Dennis 34287d0014
Update README.md 2023-05-08 12:03:08 +02:00
Dennis ee968ff961
[Feature] Add billing cycles (#823) 2023-05-08 11:59:58 +02:00
1day2die 6bba6123c6 Update to new Settings 2023-05-08 11:55:17 +02:00
IceToast b1aaaeb329 fix: 🐛 add missing cancel functionality 2023-05-08 11:39:21 +02:00
IceToast da338aacdc fix: 🐛 Old settings relict the second 2023-05-08 11:35:28 +02:00
IceToast ad5ff40761 fix: 🐛 Old settings relict 2023-05-08 11:26:47 +02:00
Dennis 04917a495e
Finish Roles API and Seeder 2023-05-08 11:11:01 +02:00
IceToast ae9ab59dec fix: 🐛 Make migrations be compatible to installed addon 2023-05-08 11:10:51 +02:00
1day2die e6f02d0679 force the seeder 2023-05-08 11:09:49 +02:00
1day2die e965b5f5c4 PermissionsSeeder in Migration 2023-05-08 11:03:11 +02:00
IceToast 5b95dd4d13 Merge branch 'billing_system' into feature/billing_cycles
# Conflicts:
#	app/Http/Controllers/Admin/ProductController.php
#	app/Http/Controllers/ServerController.php
#	app/Models/Server.php
#	app/Models/User.php
#	lang/cs.json
#	lang/en.json
#	themes/default/views/admin/products/create.blade.php
#	themes/default/views/admin/products/edit.blade.php
#	themes/default/views/admin/settings/tabs/system.blade.php
#	themes/default/views/servers/create.blade.php
#	themes/default/views/servers/index.blade.php
#	themes/default/views/servers/settings.blade.php
2023-05-08 11:03:10 +02:00
1day2die 4a4f6bebdb Role API Update 2023-05-08 10:55:37 +02:00
1day2die 6ef0b63c36 API Fixes || Missing: Update Role 2023-05-08 10:45:47 +02:00
Dennis 72fbd34a57
Ticket information | notify groups (#821) 2023-05-07 22:37:46 +02:00
1day2die 3d8a1cf53f notify groups (check validation needed) 2023-05-07 22:36:50 +02:00
1day2die b07238cbed customizable ticket information 2023-05-07 21:57:13 +02:00
Dennis d41253a1b1
OOM Killer frontend (#820) 2023-05-07 21:41:53 +02:00
1day2die a32d42feab OOM Killer 2023-05-07 21:41:26 +02:00
Dennis 438a3374b0
Merge branch 'Ctrlpanel-gg:development' into development 2023-05-07 21:30:05 +02:00
Dennis 66039d1739
Dev no encrypt in DEV 2023-05-07 21:29:27 +02:00
IceToast bc56f713ad fix: 🐛 Call parent constructor on model 2023-05-07 21:24:26 +02:00
1day2die 8279f07156 new Permission "admin.servers.bypass_creation_enabled" 2023-05-07 19:49:05 +02:00
1day2die a2a5426252 fix cloning 2023-05-07 19:45:50 +02:00
Dennis 8c547414b4
remove "allowed" on referral. its now a permission (#818) 2023-05-07 19:07:05 +02:00
1day2die 28afc71d7f remove "allowed" on referral. its now a permission 2023-05-07 19:05:50 +02:00
Dennis 20f34165f6
fix view of partner programm (#817) 2023-05-07 18:58:31 +02:00
1day2die f72447dfa1 fix view of partner programm 2023-05-07 18:57:57 +02:00
Dennis 519a13bc2d
Cleanup Middleware, Fix locale (#816) 2023-05-07 18:43:49 +02:00
1day2die 5e46bbbe7e Cleanup Middleware, Fix locale 2023-05-07 18:43:28 +02:00
Dennis c807476ff6
Refactor "Moderator" views (#815) 2023-05-07 17:38:39 +02:00
1day2die 587e071b87 Refactor "Moderator" views 2023-05-07 17:38:25 +02:00
Dennis ea554bf288
OOM killer & Roles / Permissions API 2023-05-05 14:07:25 +02:00
1day2die 0899df5945 OOM killer & Roles / Permissions API 2023-05-05 14:06:38 +02:00
1day2die d41d0a6e2d ticket notify permission 2023-05-05 12:29:28 +02:00
1day2die 0ca052a2ba remove global verification banner 2023-05-05 12:19:33 +02:00
Dennis f8d7919a43
Update AppServiceProvider.php (#813) 2023-05-05 12:03:34 +02:00
1day2die 44db7b4847 Update AppServiceProvider.php 2023-05-05 12:01:57 +02:00
Dennis 59c04edf90
add back the github branch (#812) 2023-05-05 12:00:46 +02:00
1day2die f71b6153e6 add back the github branch 2023-05-05 12:00:10 +02:00
Dennis 98566845bd
App Version (#811) 2023-05-05 11:55:47 +02:00
1day2die 57178da067 Update app.php 2023-05-05 11:54:12 +02:00
1day2die e7f19af9f9 MOTD 2023-05-05 11:38:00 +02:00
Dennis dd9fc5aa9b
Role Powers (#810) 2023-05-05 11:18:32 +02:00
1day2die 7d974d3408 role powers 2023-05-05 11:17:40 +02:00
Dennis 6687186fd4
full settings permissions (#809) 2023-05-05 11:00:08 +02:00
1day2die a573454810 full settings permissions 2023-05-05 10:59:15 +02:00
Dennis fa24a6559b
Perms 90% done(#808) 2023-05-05 01:29:34 +02:00
1day2die 03eef835f7 simple settings perms 2023-05-05 01:28:56 +02:00
1day2die 79432fce82 Permission on sidebar 2023-05-05 01:21:19 +02:00
1day2die 4c780deb02 All permissions except Settings 2023-05-05 01:11:55 +02:00
Dennis 88bb1395ea
disable invoices by default || full frontend roles shown 2023-05-03 16:17:31 +02:00
1day2die 804a800d4f full frontend perms showing roles 2023-05-03 16:16:50 +02:00
1day2die 5d288cd070 Update UserPayment.php 2023-05-03 16:05:55 +02:00
1day2die 04a4f2ab96 disable invoices by default 2023-05-03 15:44:22 +02:00
Dennis 75af4c1e8f
Merge pull request #93 from Ctrlpanel-gg/dev_no-encryption
Dev no encryption
2023-05-03 15:22:45 +02:00
Dennis a4b802099c
Fix extension helper os (#805) 2023-05-03 15:20:29 +02:00
IceToast d51d5c66bf
Merge branch 'dev_no-encryption' into fix_extensionHelper_OS 2023-05-03 15:18:31 +02:00
IceToast 7754041532 fix: 🐛 hotfix migrations 2023-05-03 15:17:35 +02:00
IceToast a4280a6fba fix: 🐛 Path seperator replacement -> be filesystem aware 2023-05-03 15:17:13 +02:00
Dennis 05173fd9c1
Development (#804) 2023-05-03 15:11:11 +02:00
1day2die 7d93d45197 Update 2023_02_01_182158_create_website_settings.php 2023-05-03 14:52:34 +02:00
1day2die eb3afbfaab full user permissions 2023-05-03 09:53:41 +02:00
Dennis 633a3d4dfa
Use IDs instead of Role Names, Fix installer, Fix Register 2023-04-30 12:21:43 +02:00
1day2die e8c8d1c68d Use IDs instead of Role Names 2023-04-30 12:21:01 +02:00
1day2die c9c991d164 allow editing name of member and admin. should be fine. we are using IDs 2023-04-30 12:17:02 +02:00
1day2die fd2e65f8b3 fix rolecontroller 2023-04-30 12:15:42 +02:00
1day2die 83d7590b60 fix installer, new users get role 2023-04-30 12:13:01 +02:00
Dennis 3ff60bf4ff
Very Basic Roles without Permissions (#800) 2023-04-30 02:35:01 +02:00
1day2die 490bc64f41 basic roles 2023-04-30 02:34:11 +02:00
1day2die 0ffceb535d simple starting permissions. only admin 2023-04-30 02:22:59 +02:00
Dennis ee96971fc6
Development (#799) 2023-04-30 01:15:20 +02:00
1day2die 02798b2c30 installer fix 2023-04-30 01:12:49 +02:00
1day2die bfda91516a add recaptcha to tickets and preview to settings 2023-04-30 01:00:45 +02:00
Dennis 03bad4f75b
Development (#798) 2023-04-30 00:49:13 +02:00
1day2die fe2020f817 Update README.md 2023-04-30 00:47:15 +02:00
1day2die 797d871b63 fix double useful links? 2023-04-30 00:39:57 +02:00
1day2die bcf238d6b7 rename controlpanel -> ctrlpanel 2023-04-30 00:38:47 +02:00
1day2die 50d0e9a37d replace controlpanel.gg -> ctrlpanel.gg 2023-04-30 00:31:32 +02:00
1day2die 0bde32bd2f remove encryption to continue working // FIX INSTALLER [value] -> [payload] 2023-04-30 00:17:40 +02:00
1day2die c1165c4561 update sweetalert to local 2023-04-29 23:39:59 +02:00
1day2die 4f42c23a4e update alpine to local 2023-04-29 23:38:20 +02:00
1day2die c8ce65c8e3 Create v1.13.4 2023-04-29 23:35:31 +02:00
1day2die a4b56fa358 update datatables 2023-04-29 23:35:08 +02:00
1day2die 971226ee8e update composer dependencies 2023-04-29 23:30:35 +02:00
Dennis 67d4603676
Small Things to Sync (#796) 2023-04-25 09:16:15 +02:00
IceToast c44686ceda
Merge branch 'main' into billing_system 2023-04-12 19:14:38 +02:00
IceToast b4b86198e6
Merge branch 'main' into billing_system 2023-03-09 11:58:17 +01:00
IceToast d6341d41c9
Merge branch 'billing_system' of github.com:IceToast/cpgg_monthly_billing_addon into billing_system 2023-02-06 14:47:32 +01:00
IceToast 255671e20d
fix: 🐛 Merge issues 2023-02-06 14:45:44 +01:00
IceToast fda40fe8bc
fix: 🐛 Merge missing hidden product in form 2023-02-06 14:38:02 +01:00
IceToast 27d2a48e10
chore: doc 2023-02-06 14:36:00 +01:00
IceToast ae787dfc4c
chore: 🌐 localization 2023-02-06 14:36:00 +01:00
IceToast 94cbea528c
Fixed Upgrade/Downgrade Credit withdrawal 2023-02-06 14:35:35 +01:00
IceToast 56aad2a7cb
fix: Number formatting on server overview (price) 2023-02-06 14:32:49 +01:00
IceToast 973e77569a
chore: Docs 2023-02-06 14:30:44 +01:00
IceToast d56bc09ac5
fix: 💄 Styling of server buttons 2023-02-06 14:30:44 +01:00
IceToast cf62cdef1f
fix: 🐛 See last commit 2023-02-06 14:30:14 +01:00
IceToast 35734579ae
fix: 🐛 Server creation fail when server.price = user.credits 2023-02-06 14:29:55 +01:00
IceToast 87ec49008d
fix: 🚑️ Fixed credits check at server creation & formatted prices nicely 2023-02-06 14:29:55 +01:00
IceToast e254b2acfe
feat: Added Quartely billing period 2023-02-06 14:21:16 +01:00
IceToast 8565faa40b
chore: 🌐 Added Bold text for No refund 2023-02-06 14:21:16 +01:00
IceToast 3f4cae352a
fix: 🐛 Disable cancel button when cancelled 2023-02-06 14:21:15 +01:00
IceToast e4165181fe
fix: 🐛 Disable Cancel Button 2023-02-06 14:21:15 +01:00
IceToast e310dba243
fix: 🐛 Credt usage at dashboard 2023-02-06 14:21:15 +01:00
IceToast 54e14d5f2b
fix: 🐛 Fiy suspended typo & localization 2023-02-06 14:20:56 +01:00
IceToast 8a4273a8fb
style: 💄 Changed Next Billing Cycle behaviour 2023-02-06 14:20:56 +01:00
IceToast ecfa160450
fix: 💄 Added hyphens at next billing cycle field 2023-02-06 14:20:56 +01:00
IceToast 2c4b3ea03e
chore: 🌐 Localization 2023-02-06 14:20:55 +01:00
IceToast 9ad2954f0e
fix: 📝 Undo Naming 2023-02-06 14:20:33 +01:00
IceToast 34bd88a4f6
refactor: 🚚 Rename Yearly -> Annually 2023-02-06 14:20:33 +01:00
IceToast 759ba59988
feat: Added yearly and half-yearly billing periods 2023-02-06 14:20:33 +01:00
IceToast 7fa9bf2062
feat: Added Server Cancelation route method and charging 2023-02-06 14:20:32 +01:00
IceToast f8c33d43be
fix: 🐛 Typo 2023-02-06 14:18:47 +01:00
IceToast 642fef6864
feat: Added Cancel Button & Next Billing Cycle field 2023-02-06 14:18:47 +01:00
IceToast 994314dbe4
feat: Added migrations undo last patch db update & add cancelation of servers 2023-02-06 14:18:47 +01:00
IceToast dfdba1e26e
fix: 🚑️ No product available at creation time 2023-02-06 14:18:47 +01:00
IceToast 35e54be155
docs: 📝 Updated Export doc 2023-02-06 14:18:47 +01:00
IceToast b1fc1f8fab
docs: 📝 Added Addon Docs 2023-02-06 14:18:46 +01:00
IceToast 117b75d3b7
feat: Added different billing periods to servers overview page 2023-02-06 14:18:23 +01:00
IceToast 907fb74734
fix: 🚑️ ChargeServer Command 2023-02-06 14:17:52 +01:00
IceToast 2e6c03e3cc
feat: Update last_billed to current time on unsuspend server 2023-02-06 14:17:52 +01:00
IceToast da0dd37559
feat: Added last_billed to server model & always charge first & fixed Charge Server command 2023-02-06 14:17:43 +01:00
IceToast 5b738be6e1
fix: 🐛 Renamed Migration & changed precision on decimals to more reasonable number 2023-02-06 14:17:01 +01:00
IceToast 3723b527f5
fix: 🚑️ decimal input steps to number inputs 2023-02-06 14:17:01 +01:00
IceToast 7e17bb62ea
feat: Added ChargeServers command & updated laravel schedule command 2023-02-06 14:16:28 +01:00
IceToast 681928c3ad
feat: Added weekly to billing_period options 2023-02-06 14:15:28 +01:00
IceToast f9d5238ea2
feat: Added billing period to server creation 2023-02-06 14:15:28 +01:00
IceToast 7dab9d3c7e
feat: Added billing period to edit, create and show of products 2023-02-06 14:14:04 +01:00
IceToast 9777e22eab
feat: Added billing_period to validation 2023-02-06 14:14:03 +01:00
IceToast 195cadc6a5
feat: 📝 Updated ProductSeeder 2023-02-06 14:13:25 +01:00
IceToast 00e525a764
feat: Added Migration -> last billed field to servers 2023-02-06 14:12:58 +01:00
IceToast 23cc70d79c
fix: 🐛 Added Migration -> Updated user credits column to be decimal 2023-02-06 14:12:58 +01:00
IceToast 88a8541623
fix: Updated Migration to calculate hourly price from existing products 2023-02-06 14:12:58 +01:00
IceToast 54325fbf3e
feat: Added Migration -> Column Billing Period 2023-02-06 14:12:58 +01:00
IceToast 25db97d0a4 fix: 🐛 Merge missing hidden product in form 2023-01-23 16:27:32 +01:00
IceToast da19554a46 Update Billing-System to V0.9
# Conflicts:
#	app/Console/Kernel.php
#	app/Http/Controllers/Admin/ProductController.php
#	app/Http/Controllers/Admin/ServerController.php
#	app/Http/Controllers/ServerController.php
#	app/Models/Server.php
#	composer.lock
#	database/seeders/Seeds/ProductSeeder.php
#	lang/bg.json
#	lang/bs.json
#	lang/cs.json
#	lang/de.json
#	lang/en.json
#	lang/es.json
#	lang/he.json
#	themes/default/views/servers/create.blade.php
#	themes/default/views/servers/index.blade.php
2023-01-23 16:19:32 +01:00
IceToast 13b9d964a4 chore: Update to latest cpgg 2022-12-19 15:13:19 +01:00
IceToast 056e41be1a chore: doc 2022-12-19 15:08:09 +01:00
IceToast a7c47bff64 chore: 🌐 localization 2022-11-22 13:43:06 +01:00
IceToast e5ae179b9d Fixed Upgrade/Downgrade Credit withdrawal 2022-11-22 13:42:46 +01:00
IceToast 0f5450e01e fix: Number formatting on server overview (price) 2022-11-22 12:23:33 +01:00
IceToast bacab7bb18 fix: 🐛 Fix cpgg bad code bug -> doesNotFit can be undefined... 2022-11-16 18:10:00 +01:00
IceToast 549b2830f9 chore: Update to latest cpgg version 2022-11-11 13:40:31 +01:00
IceToast 35479c9cfc chore: Update to newest CPGG Main version 2022-10-13 23:34:12 +02:00
IceToast 7d363246b0 Merge branch 'main' into billing_system 2022-08-21 19:56:25 +02:00
IceToast b580e47cdf chore: Docs 2022-08-15 14:56:34 +02:00
IceToast fd21665493 fix: 💄 Styling of server buttons 2022-08-15 14:52:49 +02:00
IceToast 7365cdee18 Merge branch 'main' into billing_system
# Conflicts:
#	app/Http/Controllers/ServerController.php
#	composer.lock
#	resources/lang/de.json
#	resources/lang/en.json
#	resources/views/servers/index.blade.php
2022-08-15 12:39:58 +02:00
IceToast e3e7329d31 fix: 🐛 See last commit 2022-07-28 23:33:04 +02:00
IceToast b80a8640de fix: 🐛 Server creation fail when server.price = user.credits 2022-07-28 23:20:39 +02:00
IceToast de96d6d6d3 fix: 🚑️ Fixed credits check at server creation & formatted prices nicely 2022-07-27 23:26:03 +02:00
IceToast 1fb855a684 feat: Added Quartely billing period 2022-07-22 05:31:05 +02:00
IceToast 479ccdb92c chore: 🌐 Added Bold text for No refund 2022-07-22 05:26:48 +02:00
IceToast 3e4a4f32fc fix: 🐛 Disable cancel button when cancelled 2022-07-22 05:12:06 +02:00
IceToast 4b27665150 fix: 🐛 Disable Cancel Button 2022-07-22 05:06:38 +02:00
IceToast f3856c88ba fix: 🐛 Credt usage at dashboard 2022-07-22 04:54:33 +02:00
IceToast abd8bc6b9c fix: 🐛 Fiy suspended typo & localization 2022-07-22 04:40:03 +02:00
IceToast 620a6b83e9 style: 💄 Changed Next Billing Cycle behaviour 2022-07-22 04:33:54 +02:00
IceToast 23a890ecba fix: 💄 Added hyphens at next billing cycle field 2022-07-22 04:31:00 +02:00
IceToast 737bf6e8e9 chore: 🌐 Localization 2022-07-22 04:25:23 +02:00
IceToast dbbdfaa623 fix: 📝 Undo Naming 2022-07-22 04:15:40 +02:00
IceToast 7369e8a643 refactor: 🚚 Rename Yearly -> Annually 2022-07-22 04:09:34 +02:00
IceToast 08208cab72 feat: Added yearly and half-yearly billing periods 2022-07-22 04:00:23 +02:00
IceToast 78a6787607 feat: Added Server Cancelation route method and charging 2022-07-22 03:52:49 +02:00
IceToast 32384044ea fix: 🐛 Typo 2022-07-22 03:51:05 +02:00
IceToast e207c0c550 feat: Added Cancel Button & Next Billing Cycle field 2022-07-22 02:58:08 +02:00
IceToast 690e0e7e12 feat: Added migrations undo last patch db update & add cancelation of servers 2022-07-22 01:50:45 +02:00
IceToast 16eb9eeca6 Merge branch 'main' of https://github.com/ControlPanel-gg/dashboard into billing_system 2022-07-20 23:16:41 +02:00
IceToast 43119e22a5 fix: 🚑️ No product available at creation time 2022-06-22 12:01:47 +02:00
IceToast dfa50141fb docs: 📝 Updated Export doc 2022-06-19 00:13:54 +02:00
IceToast b6775f768d docs: 📝 Added Addon Docs 2022-06-18 23:55:43 +02:00
IceToast b9b860b863 fix: 🔥 Removed charge first hour at creation -> not in use anymore 2022-06-18 23:43:58 +02:00
IceToast 609041a967 feat: Added different billing periods to servers overview page 2022-06-18 23:41:40 +02:00
IceToast 570b0b014f fix: 🚑️ ChargeServer Command 2022-06-16 19:03:59 +02:00
IceToast cad41ffb0a feat: Update last_billed to current time on unsuspend server 2022-06-16 16:18:27 +02:00
IceToast a26f6dd137 feat: Added last_billed to server model & always charge first & fixed Charge Server command 2022-06-16 16:15:57 +02:00
IceToast d7c0c26f35 fix: 🐛 Renamed Migration & changed precision on decimals to more reasonable number 2022-06-16 15:33:44 +02:00
IceToast 6c93343200 fix: 🚑️ decimal input steps to number inputs 2022-06-16 15:32:42 +02:00
IceToast fb23f43f88 feat: Added ChargeServers command & updated laravel schedule command 2022-06-16 14:54:42 +02:00
IceToast 9e8bd0a2d3 feat: Added weekly to billing_period options 2022-06-16 14:54:06 +02:00
IceToast 5c37dbcc51 feat: Added billing period to server creation 2022-06-16 12:36:01 +02:00
IceToast 4581b018b0 feat: Added billing period to edit, create and show of products 2022-06-16 12:27:53 +02:00
IceToast 87bca5c52b feat: Added billing_period to validation 2022-06-16 12:04:52 +02:00
IceToast b726326e99 feat: 📝 Updated ProductSeeder 2022-06-16 12:01:53 +02:00
IceToast d7830eeb41 feat: Added Migration -> last billed field to servers 2022-06-16 11:59:17 +02:00
IceToast 8383fa03aa fix: 🐛 Added Migration -> Updated user credits column to be decimal 2022-06-16 11:56:05 +02:00
IceToast 950278c948 fix: Updated Migration to calculate hourly price from existing products 2022-06-16 11:55:34 +02:00
IceToast 8f38b9e023 feat: Added Migration -> Column Billing Period 2022-06-16 11:32:00 +02:00
IceToast 3c25084ada chore: 📌 Updated deps 2022-06-16 11:31:34 +02:00
280 changed files with 13405 additions and 22419 deletions

View file

@ -3,9 +3,9 @@ root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_size = 2
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
@ -13,3 +13,24 @@ trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 2
[*.php]
indent_size = 4
[*.blade.php]
indent_size = 2
[*.js]
indent_size = 4
[*.jsx]
indent_size = 2
[*.tsx]
indent_size = 2
[*.json]
indent_size = 4

View file

@ -1,28 +1,27 @@
### --- App Settings --- ###
APP_NAME=Ctrlpanel.gg
APP_NAME=CtrlPanel.gg
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost
# List with timezones https://www.php.net/manual/en/timezones.php
APP_TIMEZONE=UTC
APP_TIMEZONE=UTC # List with timezones https://www.php.net/manual/en/timezones.php
### --- App Settings End --- ###
### --- DB Settings (required) --- ###
### --- Database Settings (required) --- ###
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=dashboard
DB_USERNAME=dashboarduser
DB_PASSWORD=
### --- DB Settings End --- ###
### --- Database Settings End --- ###
# Google Recaptcha API Credentials - https://www.google.com/recaptcha/admin - reCaptcha V2 (not v3)
### --- Google Recaptcha Settings --- ###
RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
RECAPTCHA_SECRET_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
### --- Google Recaptcha Settings End --- ###
# Mail Server Settings - (HOST -> SMTP Server)
### --- Mail Server Settings --- ###
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
@ -31,25 +30,22 @@ MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
### --- Mail Server Settings End --- ###
# Laravel Logging Settings - https://laravel.com/docs/5.7/logging - Not needed to be changed
### --- Logging Settings --- ###
LOG_CHANNEL=stack
LOG_LEVEL=debug
### --- Logging Settings End --- ###
# Do not change anything below this line
BROADCAST_DRIVER=log
### --- Cache and Queue Settings --- ###
CACHE_DRIVER=file
QUEUE_CONNECTION=database
SESSION_DRIVER=file
SESSION_LIFETIME=120
SETTINGS_CACHE_ENABLED=true
### --- Cache and Queue Settings End --- ###
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
### --- External Services Credentials --- ###
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
@ -59,6 +55,15 @@ PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
### --- External Services Credentials End --- ###
### --- Additional Configuration --- ###
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
### --- Additional Configuration End --- ###

5
.gitattributes vendored
View file

@ -1,5 +1,10 @@
# Automatically detect text files
* text=auto
# Vendored files for specific languages
*.css linguist-vendored
*.scss linguist-vendored
*.js linguist-vendored
# Ignore CHANGELOG.md when exporting
CHANGELOG.md export-ignore

22
.github/CODE_OF_CONDUCT.md vendored Normal file
View file

@ -0,0 +1,22 @@
## Code of Conduct
### 🤝 Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
### 🌟 Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting

34
.github/CONTRIBUTING.md vendored Normal file
View file

@ -0,0 +1,34 @@
# Contributing Guidelines
Thank you for considering contributing to this repository! Before making a contribution, please take a moment to review the following guidelines.
## 🕵️‍♂️ Finding Tasks
Check the open issues to see if there's something you can contribute to. If you have an idea or encounter a bug that's not already listed, feel free to create a new issue and wait for feedback from the development team.
## 🤝 Code of Conduct
Please adhere to our [Code of Conduct](https://github.com/Ctrlpanel-gg/panel/blob/main/.github/CODE_OF_CONDUCT.md) in all your interactions with the project.
## 🌍 Localization
If you add any strings that are displayed on the frontend, please localize them using the following format:
```
"New String" -> {{ __('New String') }}
```
After adding localized strings, run the following command to generate localization files:
```cmd
php artisan translatable:export en
```
## 🚀 Pull Request Process
1. Give your pull request (PR) a clear and descriptive title that summarizes the changes.
2. The development team will review your code and provide feedback or approve/merge it when appropriate.
3. Ensure that your PR follows our Code of Conduct and coding style guidelines.
### 💻 Coding Style
We follow the PSR12 code standard for PHP.
Thank you for your contributions! 🎉

View file

@ -1,6 +1,6 @@
name: "\U0001F41B Bug report"
description: Create a report to help us improve
title: "[Bug]: "
title: "[Bug] "
labels: ["bug"]
body:
- type: textarea
@ -31,9 +31,9 @@ body:
validations:
required: false
- type: textarea
id: ctrlpanel-logs
id: controlpanel-logs
attributes:
label: Ctrlpanel Logs
label: Controlpanel Logs
description: Please copy and paste your laravel-log output. You may also provide a link to it using the following command `tail -n 100 /var/www/controlpanel/storage/logs/laravel.log | nc pteropaste.com 99`
render: Shell
- type: textarea

View file

@ -5,4 +5,4 @@ contact_links:
about: Please visit our Discord for help with your installation.
- name: ❓ General Question
url: https://discord.gg/4Y6HjD2uyU
about: Please visit our Discord for general questions about the ControlPanel.
about: Please visit our Discord for general questions about the CtrlPanel.

37
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,37 @@
✨ Thank you for your contribution to our project! Before you submit your pull request, please take a moment to review and complete the following
⚠️ Please modify this template below and if not already done, read our pull request rules, Thanks!
Ensure that your pull request meets the following criteria:
- The code follows the style guidelines of this project
- You have performed a self-review of your own code and tested it
- You have commented your code, particularly in hard-to-understand areas
- Your changes generate no new warnings
Delete the above text and the following sections before submitting your pull request.
---
💡 **Description**
Briefly describe the purpose of your pull request, including any relevant issue numbers it addresses.
---
🛠️ **Type of Change**
Please select the appropriate type of change:
- Bug fix (non-breaking change which fixes an issue)
- User interface (UI) improvement
- New feature (non-breaking change which adds functionality)
- Breaking change (a fix or feature that would cause existing functionality to not work as expected)
- Other
- This change requires a documentation update
---
🖼️ **Screenshots (if applicable)**
If your pull request includes any visual changes, please provide screenshots here, do not use any external link.

17
.github/SECURITY.md vendored Normal file
View file

@ -0,0 +1,17 @@
# Security Policy
## Reporting a Vulnerability
🛡️ If you discover a security vulnerability, please report it to us via GitHub Advisories.
⚠️ Please refrain from using the public issue tracker or discussing the vulnerability in public channels, as it may exacerbate the issue.
## Acceptance of Bug Bounty Platforms
At this time, we only accept vulnerability reports through GitHub Advisories. We kindly ask that you do not submit reports via other third-party bug bounty platforms, as they will be disregarded.
## Supported Versions
### ControlPanel Versions
We strongly recommend using or upgrading to the latest version of ControlPanel to ensure you have access to the latest security fixes and enhancements.

34
.gitignore vendored
View file

@ -1,28 +1,34 @@
# Ignore dependencies and cache
/node_modules
/vendor
/storage/*.key
# Ignore public assets
/public/hot
/public/storage
/storage/*.key
/vendor
/storage/credit_deduction_log
storage/debugbar
/storage/app/public/logo.png
# Ignore environment files and configuration
.env
.env.testing
.env.backup
.idea
.env.dev
# Ignore testing and debug logs
.phpunit.result.cache
docker-compose.override.yml
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
yarn.lock
# Ignore Docker and Homestead configuration
docker-compose.override.yml
Homestead.json
Homestead.yaml
# Ignore gitignore itself
.gitignore
.env.dev
.env.testing
storage/invoices.zip
storage/app/public/logo.png
*vscode
- Kopie.env
# Ignore installation logs and locks
public/install/logs.txt
install.lock
public/install/logs/installer.log

View file

@ -1,43 +0,0 @@
# Contributing
When contributing to this repository, please go through the open issues to see if you can contribute to something. If you want to contribute something that is not in the issues you can make an issue and wait for response from the dev team.
Please note we have a code of conduct, please follow it in all your interactions with the project.
If you added any Strings which are displayed at the frontend please localize them (e.g. "New String" -> {{ __('New String') }}) and run the localization string generation:
```cmd
php artisan translatable:export en
```
## Pull request process
1. Give your PR a good descriptive title, so we can view immediately what the PR is about.
2. The dev team will look at your code and approve / merge when possible.
3. Make sure your PR follows our code of conduct and coding style.
## Code of Conduct
### Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
### Coding Style
We are following the PSR12 code standard for PHP.
### Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting

106
README.md
View file

@ -1,67 +1,87 @@
### Features
- PayPal Integration
- Stripe Integration
- Referral System
- Partner System
- Ticket System
- Upgrade/Downgrade Server Resources
- Store (credit system with hourly billing and invoices)
- Email Verification
- Audit Log
- Admin Dashboard
- User/Server Management
- Customisable server plans
- Vouchers
- Alert System
- Theme Support
- and so much more!
<div align="center">
<img src="https://ctrlpanel.gg/img/controlpanel.png" width="128" alt="" />
</div>
# CtrlPanel-gg
![ctrlpanel](https://user-images.githubusercontent.com/67899387/214684708-739c1d21-06e8-4dec-a4f1-81533a46cc7e.png)
CtrlPanel offers an easy-to-use and free billing solution for all starting and experienced hosting providers that seamlessly integrates with the Pterodactyl panel. It facilitates account creation, server ordering, and management, while offering addons, multiple payment methods, and customizable themes for a comprehensive solution.
![GitHub tag](https://img.shields.io/github/tag/ControlPanel-gg/dashboard)
![Overall Installations](https://img.shields.io/badge/Overall%20Installations-5000%2B-green)
![GitHub stars](https://img.shields.io/github/stars/ControlPanel-gg/dashboard)
[![Crowdin](https://badges.crowdin.net/controlpanelgg/localized.svg)](https://crowdin.com/project/controlpanelgg)
![License](https://img.shields.io/github/license/ControlPanel-gg/dashboard)
![Discord](https://img.shields.io/discord/787829714483019826)
![](https://img.shields.io/endpoint?label=v0.9%20Installations&url=https%3A%2F%2Fmarket.ctrlpanel.gg%2Fcallhome.php%3Fgetinstalls)
![](https://img.shields.io/badge/Overall%20Installations-5000%2B-green)
![](https://img.shields.io/github/stars/CtrlPanel-gg/panel) ![](https://img.shields.io/github/forks/CtrlPanel-gg/panel) ![](https://img.shields.io/github/tag/CtrlPanel-gg/panel) [![Crowdin](https://badges.crowdin.net/controlpanelgg/localized.svg)](https://crowdin.com/project/controlpanelgg) ![](https://img.shields.io/github/issues/CtrlPanel-gg/panel) ![](https://img.shields.io/github/license/CtrlPanel-gg/panel) ![](https://img.shields.io/discord/787829714483019826)
## About
![CtrlPanel](https://user-images.githubusercontent.com/67899387/214684708-739c1d21-06e8-4dec-a4f1-81533a46cc7e.png)
CtrlPanel's Dashboard is a dashboard application designed to offer clients a management tool to manage their pterodactyl servers. This dashboard comes with a credit-based billing solution that credits users hourly for each server they have and suspends them if they run out of credits.
## ⭐ Features
This dashboard offers an easy to use and free billing solution for all starting and experienced hosting providers. This dashboard has many customisation options and added discord Oauth verification to offer a solid link between your discord server and your dashboard. You can check our [Demo here](https://demo.ctrlpanel.gg "Demo").
- Store (credit system with hourly billing and invoices)
- Many Popular Payment Methods
- Referral
- Partner
- Vouchers
- Ticket
- Account Management
- Admin Dashboard and Tools
- Addon Support
- and more!
### [Installation](https://ctrlpanel.gg/docs/intro "Installation")
## ⛰️ Live Demo
### [Updating](https://ctrlpanel.gg/docs/Installation/updating "Updating")
Try it!
### [Discord](https://discord.gg/4Y6HjD2uyU "Discord")
Demo Server: [demo.CtrlPanel.gg](https://demo.CtrlPanel.gg)
### [Contributing](https://ctrlpanel.gg/docs/Contributing/contributing "Contributing")
<!-- It is a temporary live demo; all data will be deleted. -->
### [Donating](https://ctrlpanel.gg/docs/Contributing/donating "Donating")
## 🔧 How to Install
### 🐳 Docker
Soon...
# Preview
<!-- ```bash
docker run ...
```
### Server Creation
![image](https://user-images.githubusercontent.com/67899387/214687234-d1ae58c0-5667-4e99-ac39-adeaabfcc7f2.png)
CtrlPanel is now running on [0.0.0.0:3001](http://0.0.0.0:3001). Don't forget to configure the database and Pterodactyl. [Documentation](documentation link here)
### Overview
![image](https://user-images.githubusercontent.com/67899387/214685859-03c8d9e1-c685-4a07-979f-df2e88ec3931.png)
more info: [Docker](docker documentation link here) -->
### Example server products
![image](https://user-images.githubusercontent.com/67899387/214686950-218e1ede-6a1f-4e53-b3f4-fe1abc371a9c.png)
### 💪🏻 Non-Docker
### Ticket System
![image](https://user-images.githubusercontent.com/67899387/214687123-0a3d0f8f-b53c-4b0d-869a-4d5df45f5184.png)
Requirements:
### Voucher System
![image](https://user-images.githubusercontent.com/67899387/214686578-ec9f0b0f-6047-4665-835f-70594b56dfd5.png)
- Platform
- Major Linux distros such as Debian, Ubuntu, CentOS, Fedora, and ArchLinux etc.
- Windows 10 (x64), Windows Server ...
### Partner System
![image](https://user-images.githubusercontent.com/67899387/214686321-36ba97a3-4181-4e60-9ba3-c9b318fe66a8.png)
Follow the [documentation](https://ctrlpanel.gg/docs/intro) to know how to install.
### MarketPlace
If you need more functionality, check out [Marketplace](https://market.ctrlpanel.gg/resources/).
## 🆙 How to Update
Please read: [Update Instructions](https://ctrlpanel.gg/docs/Installation/updating)
## 🆕 What's Next?
Roadmap: [CtrlPanel Roadmap](https://github.com/orgs/Ctrlpanel-gg/projects/1)
## 🗣️ Discussion / Ask for Help
For any general or technical questions, join CtrlPanel Discord for finding answers to your question. If you cannot find the information you need, feel free to ask.
## 🤝 Contributing
Please read [CONTRIBUTING.md](https://github.com/Ctrlpanel-gg/panel/blob/main/.github/CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
Thanks to all contributors and supporters!
## ♥️ Donations
If you like what we do, please consider [supporting](https://ctrlpanel.gg/docs/Contributing/donating) us.

View file

@ -0,0 +1,8 @@
<?php
namespace App\Classes;
abstract class AbstractExtension
{
abstract public static function getConfig(): array;
}

View file

@ -0,0 +1,63 @@
<?php
namespace App\Classes;
use Exception;
use Illuminate\Support\Facades\DB;
use Spatie\LaravelSettings\Migrations\SettingsMigration;
abstract class LegacySettingsMigration extends SettingsMigration
{
public function getNewValue(string $name, string $group)
{
$new_value = DB::table('settings')->where([['group', '=', $group], ['name', '=', $name]])->get(['payload'])->first();
if (is_null($new_value) || is_null($new_value->payload)) {
return null;
}
// Some keys returns '""' as a value.
if ($new_value->payload === '""') {
return null;
}
// remove the quotes from the string
if (substr($new_value->payload, 0, 1) === '"' && substr($new_value->payload, -1) === '"') {
return substr($new_value->payload, 1, -1);
}
return $new_value->payload;
}
/**
* Get the old value from the settings_old table.
* @param string $key The key to get the value from table.
* @param int|string|bool|null $default The default value to return if the value is null. If value is not nullable, a default must be provided.
*/
public function getOldValue(string $key, int|string|bool|null $default = null)
{
$old_value = DB::table('settings_old')->where('key', '=', $key)->get(['value', 'type'])->first();
if (is_null($old_value) || is_null($old_value->value)) {
return $default;
}
switch ($old_value->type) {
case 'string':
case 'text':
// Edgecase: The value is a boolean, but it's stored as a string.
if ($old_value->value === "false" || $old_value->value === "true") {
return filter_var($old_value->value, FILTER_VALIDATE_BOOL);
}
return $old_value->value;
case 'boolean':
return filter_var($old_value->value, FILTER_VALIDATE_BOOL);
case 'integer':
return filter_var($old_value->value, FILTER_VALIDATE_INT);
default:
throw new Exception("Unknown type: {$old_value->type}");
}
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace App\Classes;
use App\Models\Payment;
use App\Models\ShopProduct;
abstract class PaymentExtension extends AbstractExtension
{
/**
* Returns the redirect url of the payment gateway to redirect the user to
*/
abstract public static function getRedirectUrl(Payment $payment, ShopProduct $shopProduct, string $totalPriceString): string;
}

View file

@ -2,46 +2,68 @@
namespace App\Classes;
use App\Models\Egg;
use App\Models\Nest;
use App\Models\Node;
use App\Models\Pterodactyl\Egg;
use App\Models\Pterodactyl\Nest;
use App\Models\Pterodactyl\Node;
use App\Models\Product;
use App\Models\Server;
use App\Models\User;
use Exception;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use App\Settings\PterodactylSettings;
use App\Settings\ServerSettings;
class Pterodactyl
class PterodactylClient
{
//TODO: Extend error handling (maybe logger for more errors when debugging)
private int $per_page_limit = 200;
private int $allocation_limit = 200;
public PendingRequest $client;
public PendingRequest $application;
public function __construct(PterodactylSettings $ptero_settings)
{
$server_settings = new ServerSettings();
try {
$this->client = $this->client($ptero_settings);
$this->application = $this->clientAdmin($ptero_settings);
$this->per_page_limit = $ptero_settings->per_page_limit;
$this->allocation_limit = $server_settings->allocation_limit;
} catch (Exception $exception) {
logger('Failed to construct Pterodactyl client, Settings table not available?', ['exception' => $exception]);
}
}
/**
* @return PendingRequest
*/
public static function client()
public function client(PterodactylSettings $ptero_settings)
{
return Http::withHeaders([
'Authorization' => 'Bearer ' . config('SETTINGS::SYSTEM:PTERODACTYL:TOKEN'),
'Authorization' => 'Bearer ' . $ptero_settings->user_token,
'Content-type' => 'application/json',
'Accept' => 'Application/vnd.pterodactyl.v1+json',
])->baseUrl(config('SETTINGS::SYSTEM:PTERODACTYL:URL') . '/api');
])->baseUrl($ptero_settings->getUrl() . 'api' . '/');
}
public static function clientAdmin()
public function clientAdmin(PterodactylSettings $ptero_settings)
{
return Http::withHeaders([
'Authorization' => 'Bearer ' . config('SETTINGS::SYSTEM:PTERODACTYL:ADMIN_USER_TOKEN'),
'Authorization' => 'Bearer ' . $ptero_settings->admin_token,
'Content-type' => 'application/json',
'Accept' => 'Application/vnd.pterodactyl.v1+json',
])->baseUrl(config('SETTINGS::SYSTEM:PTERODACTYL:URL') . '/api');
])->baseUrl($ptero_settings->getUrl() . 'api' . '/');
}
/**
* @return Exception
*/
private static function getException(string $message = '', int $status = 0): Exception
private function getException(string $message = '', int $status = 0): Exception
{
if ($status == 404) {
return new Exception('Ressource does not exist on pterodactyl - ' . $message, 404);
@ -68,10 +90,10 @@ class Pterodactyl
*
* @throws Exception
*/
public static function getEggs(Nest $nest)
public function getEggs(Nest $nest)
{
try {
$response = self::client()->get("/application/nests/{$nest->id}/eggs?include=nest,variables&per_page=" . config('SETTINGS::SYSTEM:PTERODACTYL:PER_PAGE_LIMIT'));
$response = $this->application->get("application/nests/{$nest->id}/eggs?include=nest,variables&per_page=" . $this->per_page_limit);
} catch (Exception $e) {
throw self::getException($e->getMessage());
}
@ -87,10 +109,10 @@ class Pterodactyl
*
* @throws Exception
*/
public static function getNodes()
public function getNodes()
{
try {
$response = self::client()->get('/application/nodes?per_page=' . config('SETTINGS::SYSTEM:PTERODACTYL:PER_PAGE_LIMIT'));
$response = $this->application->get('application/nodes?per_page=' . $this->per_page_limit);
} catch (Exception $e) {
throw self::getException($e->getMessage());
}
@ -107,10 +129,10 @@ class Pterodactyl
* @throws Exception
* @description Returns the infos of a single node
*/
public static function getNode($id)
public function getNode($id)
{
try {
$response = self::client()->get('/application/nodes/' . $id);
$response = $this->application->get('application/nodes/' . $id);
} catch (Exception $e) {
throw self::getException($e->getMessage());
}
@ -121,10 +143,10 @@ class Pterodactyl
return $response->json()['attributes'];
}
public static function getServers()
public function getServers()
{
try {
$response = self::client()->get('/application/servers?per_page=' . config('SETTINGS::SYSTEM:PTERODACTYL:PER_PAGE_LIMIT'));
$response = $this->application->get('application/servers?per_page=' . $this->per_page_limit);
} catch (Exception $e) {
throw self::getException($e->getMessage());
}
@ -140,10 +162,10 @@ class Pterodactyl
*
* @throws Exception
*/
public static function getNests()
public function getNests()
{
try {
$response = self::client()->get('/application/nests?per_page=' . config('SETTINGS::SYSTEM:PTERODACTYL:PER_PAGE_LIMIT'));
$response = $this->application->get('application/nests?per_page=' . $this->per_page_limit);
} catch (Exception $e) {
throw self::getException($e->getMessage());
}
@ -159,10 +181,10 @@ class Pterodactyl
*
* @throws Exception
*/
public static function getLocations()
public function getLocations()
{
try {
$response = self::client()->get('/application/locations?per_page=' . config('SETTINGS::SYSTEM:PTERODACTYL:PER_PAGE_LIMIT'));
$response = $this->application->get('application/locations?per_page=' . $this->per_page_limit);
} catch (Exception $e) {
throw self::getException($e->getMessage());
}
@ -179,7 +201,7 @@ class Pterodactyl
*
* @throws Exception
*/
public static function getFreeAllocationId(Node $node)
public function getFreeAllocationId(Node $node)
{
return self::getFreeAllocations($node)[0]['attributes']['id'] ?? null;
}
@ -190,7 +212,7 @@ class Pterodactyl
*
* @throws Exception
*/
public static function getFreeAllocations(Node $node)
public function getFreeAllocations(Node $node)
{
$response = self::getAllocations($node);
$freeAllocations = [];
@ -214,11 +236,10 @@ class Pterodactyl
*
* @throws Exception
*/
public static function getAllocations(Node $node)
public function getAllocations(Node $node)
{
$per_page = config('SETTINGS::SERVER:ALLOCATION_LIMIT', 200);
try {
$response = self::client()->get("/application/nodes/{$node->id}/allocations?per_page={$per_page}");
$response = $this->application->get("application/nodes/{$node->id}/allocations?per_page={$this->allocation_limit}");
} catch (Exception $e) {
throw self::getException($e->getMessage());
}
@ -229,24 +250,15 @@ class Pterodactyl
return $response->json();
}
/**
* @param string $route
* @return string
*/
public static function url(string $route): string
{
return config('SETTINGS::SYSTEM:PTERODACTYL:URL') . $route;
}
/**
* @param Server $server
* @param Egg $egg
* @param int $allocationId
* @return Response
*/
public static function createServer(Server $server, Egg $egg, int $allocationId)
public function createServer(Server $server, Egg $egg, int $allocationId)
{
return self::client()->post('/application/servers', [
return $this->application->post('application/servers', [
'name' => $server->name,
'external_id' => $server->id,
'user' => $server->user->pterodactyl_id,
@ -254,6 +266,7 @@ class Pterodactyl
'docker_image' => $egg->docker_image,
'startup' => $egg->startup,
'environment' => $egg->getEnvironmentVariables(),
'oom_disabled' => !$server->product->oom_killer,
'limits' => [
'memory' => $server->product->memory,
'swap' => $server->product->swap,
@ -272,10 +285,10 @@ class Pterodactyl
]);
}
public static function suspendServer(Server $server)
public function suspendServer(Server $server)
{
try {
$response = self::client()->post("/application/servers/$server->pterodactyl_id/suspend");
$response = $this->application->post("application/servers/$server->pterodactyl_id/suspend");
} catch (Exception $e) {
throw self::getException($e->getMessage());
}
@ -286,10 +299,10 @@ class Pterodactyl
return $response;
}
public static function unSuspendServer(Server $server)
public function unSuspendServer(Server $server)
{
try {
$response = self::client()->post("/application/servers/$server->pterodactyl_id/unsuspend");
$response = $this->application->post("application/servers/$server->pterodactyl_id/unsuspend");
} catch (Exception $e) {
throw self::getException($e->getMessage());
}
@ -309,7 +322,7 @@ class Pterodactyl
public function getUser(int $pterodactylId)
{
try {
$response = self::client()->get("/application/users/{$pterodactylId}");
$response = $this->application->get("application/users/{$pterodactylId}");
} catch (Exception $e) {
throw self::getException($e->getMessage());
}
@ -326,10 +339,10 @@ class Pterodactyl
* @param int $pterodactylId
* @return mixed
*/
public static function getServerAttributes(int $pterodactylId, bool $deleteOn404 = false)
public function getServerAttributes(int $pterodactylId, bool $deleteOn404 = false)
{
try {
$response = self::client()->get("/application/servers/{$pterodactylId}?include=egg,node,nest,location");
$response = $this->application->get("application/servers/{$pterodactylId}?include=egg,node,nest,location");
} catch (Exception $e) {
throw self::getException($e->getMessage());
}
@ -356,9 +369,9 @@ class Pterodactyl
* @param Product $product
* @return Response
*/
public static function updateServer(Server $server, Product $product)
public function updateServer(Server $server, Product $product)
{
return self::client()->patch("/application/servers/{$server->pterodactyl_id}/build", [
return $this->application->patch("application/servers/{$server->pterodactyl_id}/build", [
'allocation' => $server->allocation,
'memory' => $product->memory,
'swap' => $product->swap,
@ -366,6 +379,7 @@ class Pterodactyl
'io' => $product->io,
'cpu' => $product->cpu,
'threads' => null,
'oom_disabled' => !$server->product->oom_killer,
'feature_limits' => [
'databases' => $product->databases,
'backups' => $product->backups,
@ -381,9 +395,9 @@ class Pterodactyl
* @param Server $server
* @return mixed
*/
public static function updateServerOwner(Server $server, int $userId)
public function updateServerOwner(Server $server, int $userId)
{
return self::client()->patch("/application/servers/{$server->pterodactyl_id}/details", [
return $this->application->patch("application/servers/{$server->pterodactyl_id}/details", [
'name' => $server->name,
'user' => $userId,
]);
@ -396,9 +410,9 @@ class Pterodactyl
* @param string $action
* @return Response
*/
public static function powerAction(Server $server, $action)
public function powerAction(Server $server, $action)
{
return self::clientAdmin()->post("/client/servers/{$server->identifier}/power", [
return $this->client->post("client/servers/{$server->identifier}/power", [
'signal' => $action,
]);
}
@ -406,9 +420,9 @@ class Pterodactyl
/**
* Get info about user
*/
public static function getClientUser()
public function getClientUser()
{
return self::clientAdmin()->get('/client/account');
return $this->client->get('client/account');
}
/**
@ -419,10 +433,10 @@ class Pterodactyl
* @param int $requireDisk
* @return bool
*/
public static function checkNodeResources(Node $node, int $requireMemory, int $requireDisk)
public function checkNodeResources(Node $node, int $requireMemory, int $requireDisk)
{
try {
$response = self::client()->get("/application/nodes/{$node->id}");
$response = $this->application->get("application/nodes/{$node->id}");
} catch (Exception $e) {
throw self::getException($e->getMessage());
}

View file

@ -1,47 +0,0 @@
<?php
namespace App\Classes\Settings;
use App\Models\Settings;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class Invoices
{
public function __construct()
{
}
public function updateSettings(Request $request)
{
$request->validate([
'logo' => 'nullable|max:10000|mimes:jpg,png,jpeg',
]);
$values = [
//SETTINGS::VALUE => REQUEST-VALUE (coming from the html-form)
'SETTINGS::INVOICE:COMPANY_NAME' => 'company-name',
'SETTINGS::INVOICE:COMPANY_ADDRESS' => 'company-address',
'SETTINGS::INVOICE:COMPANY_PHONE' => 'company-phone',
'SETTINGS::INVOICE:COMPANY_MAIL' => 'company-mail',
'SETTINGS::INVOICE:COMPANY_VAT' => 'company-vat',
'SETTINGS::INVOICE:COMPANY_WEBSITE' => 'company-web',
'SETTINGS::INVOICE:PREFIX' => 'invoice-prefix',
'SETTINGS::INVOICE:ENABLED' => 'enable-invoices',
];
foreach ($values as $key => $value) {
$param = $request->get($value);
Settings::where('key', $key)->updateOrCreate(['key' => $key], ['value' => $param]);
Cache::forget('setting'.':'.$key);
}
if ($request->hasFile('logo')) {
$request->file('logo')->storeAs('public', 'logo.png');
}
return redirect(route('admin.settings.index').'#invoices')->with('success', __('Invoice settings updated!'));
}
}

View file

@ -1,56 +0,0 @@
<?php
namespace App\Classes\Settings;
use App\Models\Settings;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;
class Language
{
public function __construct()
{
}
public function updateSettings(Request $request)
{
$validator = Validator::make($request->all(), [
'autotranslate' => 'string',
'canClientChangeLanguage' => 'string',
'defaultLanguage' => 'required|string',
'languages' => 'required|array',
'languages.*' => 'required|string',
'datatable-language' => 'required|string',
]);
if ($validator->fails()) {
return redirect(route('admin.settings.index').'#language')->with('error', __('Language settings have not been updated!'))->withErrors($validator);
}
$values = [
//SETTINGS::VALUE => REQUEST-VALUE (coming from the html-form)
'SETTINGS::LOCALE:DEFAULT' => 'defaultLanguage',
'SETTINGS::LOCALE:DYNAMIC' => 'autotranslate',
'SETTINGS::LOCALE:CLIENTS_CAN_CHANGE' => 'canClientChangeLanguage',
'SETTINGS::LOCALE:AVAILABLE' => 'languages',
'SETTINGS::LOCALE:DATATABLES' => 'datatable-language',
];
foreach ($values as $key => $value) {
$param = $request->get($value);
if (is_array($param)) {
$param = implode(',', $param);
}
Settings::where('key', $key)->updateOrCreate(['key' => $key], ['value' => $param]);
Cache::forget('setting'.':'.$key);
Session::remove('locale');
}
return redirect(route('admin.settings.index').'#language')->with('success', __('Language settings updated!'));
}
}

View file

@ -1,107 +0,0 @@
<?php
namespace App\Classes\Settings;
use App\Models\Settings;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
class Misc
{
public function __construct()
{
}
public function updateSettings(Request $request)
{
$validator = Validator::make($request->all(), [
'icon' => 'nullable|max:10000|mimes:jpg,png,jpeg',
'favicon' => 'nullable|max:10000|mimes:ico',
'discord-bot-token' => 'nullable|string',
'discord-client-id' => 'nullable|string',
'discord-client-secret' => 'nullable|string',
'discord-guild-id' => 'nullable|string',
'discord-invite-url' => 'nullable|string',
'discord-role-id' => 'nullable|string',
'recaptcha-site-key' => 'nullable|string',
'recaptcha-secret-key' => 'nullable|string',
'enable-recaptcha' => 'nullable|string',
'mailservice' => 'nullable|string',
'mailhost' => 'nullable|string',
'mailport' => 'nullable|string',
'mailusername' => 'nullable|string',
'mailpassword' => 'nullable|string',
'mailencryption' => 'nullable|string',
'mailfromadress' => 'nullable|string',
'mailfromname' => 'nullable|string',
'enable_referral' => 'nullable|string',
'referral_reward' => 'nullable|numeric',
'referral_allowed' => 'nullable|string',
'always_give_commission' => 'nullable|string',
'referral_percentage' => 'nullable|numeric',
'referral_mode' => 'nullable|string',
'ticket_enabled' => 'nullable|string',
'ticket_notify' => 'string',
]);
$validator->after(function ($validator) use ($request) {
// if enable-recaptcha is true then recaptcha-site-key and recaptcha-secret-key must be set
if ($request->get('enable-recaptcha') == 'true' && (! $request->get('recaptcha-site-key') || ! $request->get('recaptcha-secret-key'))) {
$validator->errors()->add('recaptcha-site-key', 'The site key is required if recaptcha is enabled.');
$validator->errors()->add('recaptcha-secret-key', 'The secret key is required if recaptcha is enabled.');
}
});
if ($validator->fails()) {
return redirect(route('admin.settings.index').'#misc')->with('error', __('Misc settings have not been updated!'))->withErrors($validator)
->withInput();
}
if ($request->hasFile('icon')) {
$request->file('icon')->storeAs('public', 'icon.png');
}
if ($request->hasFile('favicon')) {
$request->file('favicon')->storeAs('public', 'favicon.ico');
}
$values = [
'SETTINGS::DISCORD:BOT_TOKEN' => 'discord-bot-token',
'SETTINGS::DISCORD:CLIENT_ID' => 'discord-client-id',
'SETTINGS::DISCORD:CLIENT_SECRET' => 'discord-client-secret',
'SETTINGS::DISCORD:GUILD_ID' => 'discord-guild-id',
'SETTINGS::DISCORD:INVITE_URL' => 'discord-invite-url',
'SETTINGS::DISCORD:ROLE_ID' => 'discord-role-id',
'SETTINGS::RECAPTCHA:SITE_KEY' => 'recaptcha-site-key',
'SETTINGS::RECAPTCHA:SECRET_KEY' => 'recaptcha-secret-key',
'SETTINGS::RECAPTCHA:ENABLED' => 'enable-recaptcha',
'SETTINGS::MAIL:MAILER' => 'mailservice',
'SETTINGS::MAIL:HOST' => 'mailhost',
'SETTINGS::MAIL:PORT' => 'mailport',
'SETTINGS::MAIL:USERNAME' => 'mailusername',
'SETTINGS::MAIL:PASSWORD' => 'mailpassword',
'SETTINGS::MAIL:ENCRYPTION' => 'mailencryption',
'SETTINGS::MAIL:FROM_ADDRESS' => 'mailfromadress',
'SETTINGS::MAIL:FROM_NAME' => 'mailfromname',
'SETTINGS::REFERRAL::ENABLED' => 'enable_referral',
'SETTINGS::REFERRAL::REWARD' => 'referral_reward',
'SETTINGS::REFERRAL::ALLOWED' => 'referral_allowed',
'SETTINGS::REFERRAL:MODE' => 'referral_mode',
'SETTINGS::REFERRAL::ALWAYS_GIVE_COMMISSION' => 'always_give_commission',
'SETTINGS::REFERRAL:PERCENTAGE' => 'referral_percentage',
'SETTINGS::TICKET:ENABLED' => 'ticket_enabled',
'SETTINGS::TICKET:NOTIFY' => 'ticket_notify',
];
foreach ($values as $key => $value) {
$param = $request->get($value);
Settings::where('key', $key)->updateOrCreate(['key' => $key], ['value' => $param]);
Cache::forget('setting'.':'.$key);
}
return redirect(route('admin.settings.index').'#misc')->with('success', __('Misc settings updated!'));
}
}

View file

@ -1,58 +0,0 @@
<?php
namespace App\Classes\Settings;
use App\Models\Settings;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
class Payments
{
public function __construct()
{
}
public function updateSettings(Request $request)
{
$validator = Validator::make($request->all(), [
'paypal-client_id' => 'nullable|string',
'paypal-client-secret' => 'nullable|string',
'paypal-sandbox-secret' => 'nullable|string',
'stripe-secret-key' => 'nullable|string',
'stripe-endpoint-secret' => 'nullable|string',
'stripe-test-secret-key' => 'nullable|string',
'stripe-test-endpoint-secret' => 'nullable|string',
'stripe-methods' => 'nullable|string',
'sales-tax' => 'nullable|numeric',
]);
if ($validator->fails()) {
return redirect(route('admin.settings.index').'#payment')->with('error', __('Payment settings have not been updated!'))->withErrors($validator)
->withInput();
}
$values = [
//SETTINGS::VALUE => REQUEST-VALUE (coming from the html-form)
'SETTINGS::PAYMENTS:PAYPAL:SECRET' => 'paypal-client-secret',
'SETTINGS::PAYMENTS:PAYPAL:CLIENT_ID' => 'paypal-client-id',
'SETTINGS::PAYMENTS:PAYPAL:SANDBOX_SECRET' => 'paypal-sandbox-secret',
'SETTINGS::PAYMENTS:PAYPAL:SANDBOX_CLIENT_ID' => 'paypal-sandbox-id',
'SETTINGS::PAYMENTS:STRIPE:SECRET' => 'stripe-secret',
'SETTINGS::PAYMENTS:STRIPE:ENDPOINT_SECRET' => 'stripe-endpoint-secret',
'SETTINGS::PAYMENTS:STRIPE:TEST_SECRET' => 'stripe-test-secret',
'SETTINGS::PAYMENTS:STRIPE:ENDPOINT_TEST_SECRET' => 'stripe-endpoint-test-secret',
'SETTINGS::PAYMENTS:STRIPE:METHODS' => 'stripe-methods',
'SETTINGS::PAYMENTS:SALES_TAX' => 'sales-tax',
];
foreach ($values as $key => $value) {
$param = $request->get($value);
Settings::where('key', $key)->updateOrCreate(['key' => $key], ['value' => $param]);
Cache::forget('setting'.':'.$key);
}
return redirect(route('admin.settings.index').'#payment')->with('success', __('Payment settings updated!'));
}
}

View file

@ -1,154 +0,0 @@
<?php
namespace App\Classes\Settings;
use App\Classes\Pterodactyl;
use App\Models\Settings;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
use Qirolab\Theme\Theme;
class System
{
public function __construct()
{
}
public function checkPteroClientkey()
{
$response = Pterodactyl::getClientUser();
if ($response->failed()) {
return redirect()->back()->with('error', __('Your Key or URL is not correct'));
}
return redirect()->back()->with('success', __('Everything is good!'));
}
public function updateSettings(Request $request)
{
$validator = Validator::make($request->all(), [
'register-ip-check' => 'string',
'server-create-charge-first-hour' => 'string',
'credits-display-name' => 'required|string',
'allocation-limit' => 'required|min:0|integer',
'force-email-verification' => 'string',
'force-discord-verification' => 'string',
'initial-credits' => 'required|min:0|integer',
'initial-server-limit' => 'required|min:0|integer',
'credits-reward-amount-discord' => 'required|min:0|integer',
'credits-reward-amount-email' => 'required|min:0|integer',
'server-limit-discord' => 'required|min:0|integer',
'server-limit-email' => 'required|min:0|integer',
'server-limit-purchase' => 'required|min:0|integer',
'pterodactyl-api-key' => 'required|string',
'pterodactyl-url' => 'required|string',
'per-page-limit' => 'required|min:0|integer',
'pterodactyl-admin-api-key' => 'required|string',
'enable-upgrades' => 'string',
'enable-disable-servers' => 'string',
'enable-disable-new-users' => 'string',
'show-imprint' => 'string',
'show-privacy' => 'string',
'show-tos' => 'string',
'alert-enabled' => 'string',
'alter-type' => 'string',
'alert-message' => 'string|nullable',
'motd-enabled' => 'string',
'usefullinks-enabled' => 'string',
'motd-message' => 'string|nullable',
'seo-title' => 'string|nullable',
'seo-description' => 'string|nullable',
]);
$validator->after(function ($validator) use ($request) {
// if enable-recaptcha is true then recaptcha-site-key and recaptcha-secret-key must be set
if ($request->get('enable-upgrades') == 'true' && (! $request->get('pterodactyl-admin-api-key'))) {
$validator->errors()->add('pterodactyl-admin-api-key', 'The admin api key is required when upgrades are enabled.');
}
});
if ($validator->fails()) {
return redirect(route('admin.settings.index').'#system')->with('error', __('System settings have not been updated!'))->withErrors($validator)
->withInput();
}
// update Icons from request
$this->updateIcons($request);
$values = [
"SETTINGS::SYSTEM:REGISTER_IP_CHECK" => "register-ip-check",
"SETTINGS::SYSTEM:SERVER_CREATE_CHARGE_FIRST_HOUR" => "server-create-charge-first-hour",
"SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME" => "credits-display-name",
"SETTINGS::SERVER:ALLOCATION_LIMIT" => "allocation-limit",
"SETTINGS::USER:MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER" => "minimum-credits",
"SETTINGS::USER:FORCE_DISCORD_VERIFICATION" => "force-discord-verification",
"SETTINGS::USER:FORCE_EMAIL_VERIFICATION" => "force-email-verification",
"SETTINGS::USER:INITIAL_CREDITS" => "initial-credits",
"SETTINGS::USER:INITIAL_SERVER_LIMIT" => "initial-server-limit",
"SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_DISCORD" => "credits-reward-amount-discord",
"SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_EMAIL" => "credits-reward-amount-email",
"SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD" => "server-limit-discord",
"SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL" => "server-limit-email",
"SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE" => "server-limit-purchase",
"SETTINGS::MISC:PHPMYADMIN:URL" => "phpmyadmin-url",
"SETTINGS::SYSTEM:PTERODACTYL:URL" => "pterodactyl-url",
'SETTINGS::SYSTEM:PTERODACTYL:PER_PAGE_LIMIT' => "per-page-limit",
"SETTINGS::SYSTEM:PTERODACTYL:TOKEN" => "pterodactyl-api-key",
"SETTINGS::SYSTEM:ENABLE_LOGIN_LOGO" => "enable-login-logo",
"SETTINGS::SYSTEM:PTERODACTYL:ADMIN_USER_TOKEN" => "pterodactyl-admin-api-key",
"SETTINGS::SYSTEM:ENABLE_UPGRADE" => "enable-upgrade",
"SETTINGS::SYSTEM:CREATION_OF_NEW_SERVERS" => "enable-disable-servers",
"SETTINGS::SYSTEM:CREATION_OF_NEW_USERS" => "enable-disable-new-users",
"SETTINGS::SYSTEM:SHOW_IMPRINT" => "show-imprint",
"SETTINGS::SYSTEM:SHOW_PRIVACY" => "show-privacy",
"SETTINGS::SYSTEM:SHOW_TOS" => "show-tos",
"SETTINGS::SYSTEM:ALERT_ENABLED" => "alert-enabled",
"SETTINGS::SYSTEM:ALERT_TYPE" => "alert-type",
"SETTINGS::SYSTEM:ALERT_MESSAGE" => "alert-message",
"SETTINGS::SYSTEM:THEME" => "theme",
"SETTINGS::SYSTEM:MOTD_ENABLED" => "motd-enabled",
"SETTINGS::SYSTEM:MOTD_MESSAGE" => "motd-message",
"SETTINGS::SYSTEM:USEFULLINKS_ENABLED" => "usefullinks-enabled",
"SETTINGS::SYSTEM:SEO_TITLE" => "seo-title",
"SETTINGS::SYSTEM:SEO_DESCRIPTION" => "seo-description",
];
foreach ($values as $key => $value) {
$param = $request->get($value);
Settings::where('key', $key)->updateOrCreate(['key' => $key], ['value' => $param]);
Cache::forget('setting'.':'.$key);
}
//SET THEME
$theme = $request->get('theme');
Theme::set($theme);
return redirect(route('admin.settings.index').'#system')->with('success', __('System settings updated!'));
}
private function updateIcons(Request $request)
{
$request->validate([
'icon' => 'nullable|max:10000|mimes:jpg,png,jpeg',
'logo' => 'nullable|max:10000|mimes:jpg,png,jpeg',
'favicon' => 'nullable|max:10000|mimes:ico',
]);
if ($request->hasFile('icon')) {
$request->file('icon')->storeAs('public', 'icon.png');
}
if ($request->hasFile('logo')) {
$request->file('logo')->storeAs('public', 'logo.png');
}
if ($request->hasFile('favicon')) {
$request->file('favicon')->storeAs('public', 'favicon.ico');
}
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace App\Console\Commands;
use App\Models\Server;
use App\Notifications\ServersSuspendedNotification;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class ChargeServers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'servers:charge';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Charge all users with severs that are due to be charged';
/**
* A list of users that have to be notified
* @var array
*/
protected $usersToNotify = [];
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
Server::whereNull('suspended')->with('user', 'product')->chunk(10, function ($servers) {
/** @var Server $server */
foreach ($servers as $server) {
/** @var Product $product */
$product = $server->product;
/** @var User $user */
$user = $server->user;
$billing_period = $product->billing_period;
// check if server is due to be charged by comparing its last_billed date with the current date and the billing period
$newBillingDate = null;
switch ($billing_period) {
case 'annually':
$newBillingDate = Carbon::parse($server->last_billed)->addYear();
break;
case 'half-annually':
$newBillingDate = Carbon::parse($server->last_billed)->addMonths(6);
break;
case 'quarterly':
$newBillingDate = Carbon::parse($server->last_billed)->addMonths(3);
break;
case 'monthly':
$newBillingDate = Carbon::parse($server->last_billed)->addMonth();
break;
case 'weekly':
$newBillingDate = Carbon::parse($server->last_billed)->addWeek();
break;
case 'daily':
$newBillingDate = Carbon::parse($server->last_billed)->addDay();
break;
case 'hourly':
$newBillingDate = Carbon::parse($server->last_billed)->addHour();
default:
$newBillingDate = Carbon::parse($server->last_billed)->addHour();
break;
};
if (!($newBillingDate->isPast())) {
continue;
}
// check if the server is canceled or if user has enough credits to charge the server or
if ($server->canceled || $user->credits <= $product->price) {
try {
// suspend server
$this->line("<fg=yellow>{$server->name}</> from user: <fg=blue>{$user->name}</> has been <fg=red>suspended!</>");
$server->suspend();
// add user to notify list
if (!in_array($user, $this->usersToNotify)) {
array_push($this->usersToNotify, $user);
}
} catch (\Exception $exception) {
$this->error($exception->getMessage());
}
return;
}
// charge credits to user
$this->line("<fg=blue>{$user->name}</> Current credits: <fg=green>{$user->credits}</> Credits to be removed: <fg=red>{$product->price}</>");
$user->decrement('credits', $product->price);
// update server last_billed date in db
DB::table('servers')->where('id', $server->id)->update(['last_billed' => $newBillingDate]);
}
return $this->notifyUsers();
});
}
/**
* @return bool
*/
public function notifyUsers()
{
if (!empty($this->usersToNotify)) {
/** @var User $user */
foreach ($this->usersToNotify as $user) {
$this->line("<fg=yellow>Notified user:</> <fg=blue>{$user->name}</>");
$user->notify(new ServersSuspendedNotification());
}
}
#reset array
$this->usersToNotify = array();
return true;
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace App\Console\Commands;
use App\Settings\CouponSettings;
use App\Models\Coupon;
use Carbon\Carbon;
use Illuminate\Console\Command;
class DeleteExpiredCoupons extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'coupons:delete';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete expired coupons from DB.';
/**
* Execute the console command.
*
* @return int
*/
public function handle(CouponSettings $couponSettings)
{
if ($couponSettings->delete_coupon_on_expires) {
$expired_coupons = Coupon::where('expires_at', '<=', Carbon::now(config('app.timezone')))->get();
foreach ($expired_coupons as $expired_coupon) {
$expired_coupon->delete();
}
}
}
}

View file

@ -2,18 +2,20 @@
namespace App\Console\Commands;
use App\Classes\Pterodactyl;
use App\Classes\PterodactylClient;
use App\Models\User;
use App\Settings\PterodactylSettings;
use App\Traits\Referral;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
class MakeUserCommand extends Command
{
use Referral;
private $pterodactyl;
/**
* The name and signature of the console command.
*
@ -28,17 +30,14 @@ class MakeUserCommand extends Command
*/
protected $description = 'Create an admin account with the Artisan Console';
private Pterodactyl $pterodactyl;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(Pterodactyl $pterodactyl)
public function __construct()
{
parent::__construct();
$this->pterodactyl = $pterodactyl;
}
@ -47,8 +46,9 @@ class MakeUserCommand extends Command
*
* @return int
*/
public function handle()
public function handle(PterodactylSettings $ptero_settings)
{
$this->pterodactyl = new PterodactylClient($ptero_settings);
$ptero_id = $this->option('ptero_id') ?? $this->ask('Please specify your Pterodactyl ID.');
$password = $this->secret('password') ?? $this->ask('Please specify your password.');
@ -101,6 +101,8 @@ class MakeUserCommand extends Command
['Referral code', $user->referral_code],
]);
$user->syncRoles(1);
return 1;
}
}

View file

@ -8,6 +8,17 @@ use Illuminate\Support\Facades\Storage;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
Commands\ChargeCreditsCommand::class,
Commands\ChargeServers::class,
Commands\DeleteExpiredCoupons::class,
];
/**
* Define the application's command schedule.
*
@ -16,9 +27,10 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('credits:charge')->hourly();
$schedule->command('servers:charge')->everyMinute();
$schedule->command('cp:versioncheck:get')->daily();
$schedule->command('payments:open:clear')->daily();
$schedule->command('coupons:delete')->daily();
//log cronjob activity
$schedule->call(function () {

View file

@ -0,0 +1,12 @@
<?php
namespace App\Enums;
// Payment status are open, processing, paid and canceled
enum PaymentStatus: String
{
case OPEN = "open";
case PROCESSING = "processing";
case PAID = "paid";
case CANCELED = "canceled";
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Events;
use App\Models\Coupon;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class CouponUsedEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public Coupon $coupon;
public string $couponCode;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(string $couponCode)
{
$this->couponCode = $couponCode;
$this->coupon = Coupon::where('code', $couponCode)->first();
}
}

View file

@ -0,0 +1,119 @@
<?php
namespace App\Extensions\PaymentGateways\Mollie;
use App\Classes\AbstractExtension;
use App\Enums\PaymentStatus;
use App\Events\PaymentEvent;
use App\Events\UserUpdateCreditsEvent;
use App\Models\PartnerDiscount;
use App\Models\Payment;
use App\Models\ShopProduct;
use App\Models\User;
use App\Models\Coupon;
use App\Traits\Coupon as CouponTrait;
use App\Events\CouponUsedEvent;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
/**
* Summary of PayPalExtension
*/
class MollieExtension extends AbstractExtension
{
use CouponTrait;
public static function getConfig(): array
{
return [
"name" => "Mollie",
"RoutesIgnoreCsrf" => [
"payment/MollieWebhook"
],
];
}
public static function getRedirectUrl(Payment $payment, ShopProduct $shopProduct, string $totalPriceString): string
{
$url = 'https://api.mollie.com/v2/payments';
$settings = new MollieSettings();
try {
$response = Http::withHeaders([
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $settings->api_key,
])->post($url, [
'amount' => [
'currency' => $shopProduct->currency_code,
'value' => $totalPriceString,
],
'description' => "Order #{$payment->id} - " . $shopProduct->name,
'redirectUrl' => route('payment.MollieSuccess'),
'cancelUrl' => route('payment.Cancel'),
'webhookUrl' => url('/extensions/payment/MollieWebhook'),
'metadata' => [
'payment_id' => $payment->id,
],
]);
if ($response->status() != 201) {
Log::error('Mollie Payment: ' . $response->body());
throw new Exception('Payment failed');
}
return $response->json()['_links']['checkout']['href'];
} catch (Exception $ex) {
Log::error('Mollie Payment: ' . $ex->getMessage());
throw new Exception('Payment failed');
}
}
static function success(Request $request): void
{
$payment = Payment::findOrFail($request->input('payment'));
$payment->status = PaymentStatus::PROCESSING;
$payment->save();
Redirect::route('home')->with('success', 'Your payment is being processed')->send();
return;
}
static function webhook(Request $request): JsonResponse
{
$url = 'https://api.mollie.com/v2/payments/' . $request->id;
$settings = new MollieSettings();
try {
$response = Http::withHeaders([
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $settings->api_key,
])->get($url);
if ($response->status() != 200) {
Log::error('Mollie Payment Webhook: ' . $response->json()['title']);
return response()->json(['success' => false]);
}
$payment = Payment::findOrFail($response->json()['metadata']['payment_id']);
$shopProduct = ShopProduct::findOrFail($payment->shop_item_product_id);
event(new PaymentEvent($payment, $payment, $shopProduct));
if ($response->json()['status'] == 'paid') {
$user = User::findOrFail($payment->user_id);
$payment->status = PaymentStatus::PAID;
$payment->save();
event(new UserUpdateCreditsEvent($user));
}
} catch (Exception $ex) {
Log::error('Mollie Payment Webhook: ' . $ex->getMessage());
return response()->json(['success' => false]);
}
// return a 200 status code
return response()->json(['success' => true]);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Extensions\PaymentGateways\Mollie;
use Spatie\LaravelSettings\Settings;
class MollieSettings extends Settings
{
public bool $enabled = false;
public ?string $api_key;
public static function group(): string
{
return 'mollie';
}
public static function getOptionInputData()
{
return [
'category_icon' => 'fas fa-dollar-sign',
'api_key' => [
'type' => 'string',
'label' => 'API Key',
'description' => 'The API Key of your Mollie App',
],
'enabled' => [
'type' => 'boolean',
'label' => 'Enabled',
'description' => 'Enable or disable this payment gateway',
],
];
}
}

View file

@ -0,0 +1,18 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
class CreateMollieSettings extends SettingsMigration
{
public function up(): void
{
$this->migrator->addEncrypted('mollie.api_key', null);
$this->migrator->add('mollie.enabled', false);
}
public function down(): void
{
$this->migrator->delete('mollie.api_key');
$this->migrator->delete('mollie.enabled');
}
}

View file

@ -0,0 +1,18 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Extensions\PaymentGateways\Mollie\MollieExtension;
Route::middleware(['web', 'auth'])->group(function () {
Route::get(
'payment/MollieSuccess',
function () {
MollieExtension::success(request());
}
)->name('payment.MollieSuccess');
});
Route::post('payment/MollieWebhook', function () {
MollieExtension::webhook(request());
})->name('payment.MollieWebhook');

View file

@ -0,0 +1,178 @@
<?php
namespace App\Extensions\PaymentGateways\PayPal;
use App\Events\CouponUsedEvent;
use App\Events\PaymentEvent;
use App\Events\UserUpdateCreditsEvent;
use App\Extensions\PaymentGateways\PayPal\PayPalSettings;
use App\Classes\PaymentExtension;
use App\Enums\PaymentStatus;
use App\Models\PartnerDiscount;
use App\Models\Payment;
use App\Models\ShopProduct;
use App\Models\User;
use App\Models\Coupon;
use App\Traits\Coupon as CouponTrait;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Log;
use PayPalCheckoutSdk\Core\PayPalHttpClient;
use PayPalCheckoutSdk\Core\ProductionEnvironment;
use PayPalCheckoutSdk\Core\SandboxEnvironment;
use PayPalCheckoutSdk\Orders\OrdersCaptureRequest;
use PayPalCheckoutSdk\Orders\OrdersCreateRequest;
use PayPalHttp\HttpException;
/**
* Summary of PayPalExtension
*/
class PayPalExtension extends PaymentExtension
{
use CouponTrait;
public static function getConfig(): array
{
return [
"name" => "PayPal",
"RoutesIgnoreCsrf" => [],
];
}
public static function getRedirectUrl(Payment $payment, ShopProduct $shopProduct, string $totalPriceString): string
{
$request = new OrdersCreateRequest();
$request->prefer('return=representation');
$request->body = [
"intent" => "CAPTURE",
"purchase_units" => [
[
"reference_id" => uniqid(),
"description" => $shopProduct->display,
"amount" => [
"value" => $totalPriceString,
'currency_code' => strtoupper($shopProduct->currency_code),
'breakdown' => [
'item_total' => [
'currency_code' => strtoupper($shopProduct->currency_code),
'value' => $totalPriceString
],
'tax_total' => [
'currency_code' => strtoupper($shopProduct->currency_code),
'value' => $shopProduct->getTaxValue(),
]
]
]
]
],
"application_context" => [
"cancel_url" => route('payment.Cancel'),
"return_url" => route('payment.PayPalSuccess', ['payment' => $payment->id]),
'brand_name' => config('app.name', 'CtrlPanel.GG'),
'shipping_preference' => 'NO_SHIPPING'
]
];
try {
// Call API with your client and get a response for your call
$response = self::getPayPalClient()->execute($request);
// check for any errors in the response
if ($response->statusCode != 201) {
throw new \Exception($response->statusCode);
}
// make sure the link is not empty
if (empty($response->result->links[1]->href)) {
throw new \Exception('No redirect link found');
}
return $response->result->links[1]->href;
} catch (HttpException $ex) {
Log::error('PayPal Payment: ' . $ex->getMessage());
throw new \Exception('PayPal Payment: ' . $ex->getMessage());
}
}
static function PaypalSuccess(Request $laravelRequest): void
{
$user = Auth::user();
$user = User::findOrFail($user->id);
$payment = Payment::findOrFail($laravelRequest->payment);
$shopProduct = ShopProduct::findOrFail($payment->shop_item_product_id);
$request = new OrdersCaptureRequest($laravelRequest->input('token'));
$request->prefer('return=representation');
try {
// Call API with your client and get a response for your call
$response = self::getPayPalClient()->execute($request);
if ($response->statusCode == 201 || $response->statusCode == 200) {
//update payment
$payment->update([
'status' => PaymentStatus::PAID,
'payment_id' => $response->result->id,
]);
event(new UserUpdateCreditsEvent($user));
event(new PaymentEvent($user, $payment, $shopProduct));
// redirect to the payment success page with success message
Redirect::route('home')->with('success', 'Payment successful')->send();
} elseif (env('APP_ENV') == 'local') {
// If call returns body in response, you can get the deserialized version from the result attribute of the response
$payment->delete();
dd($response);
} else {
$payment->update([
'status' => PaymentStatus::CANCELED,
'payment_id' => $response->result->id,
]);
abort(500);
}
} catch (HttpException $ex) {
if (env('APP_ENV') == 'local') {
echo $ex->statusCode;
$payment->delete();
dd($ex->getMessage());
} else {
$payment->update([
'status' => PaymentStatus::CANCELED,
'payment_id' => $response->result->id,
]);
abort(422);
}
}
}
static function getPayPalClient(): PayPalHttpClient
{
$environment = config('app.env') == 'local'
? new SandboxEnvironment(self::getPaypalClientId(), self::getPaypalClientSecret())
: new ProductionEnvironment(self::getPaypalClientId(), self::getPaypalClientSecret());
return new PayPalHttpClient($environment);
}
/**
* @return string
*/
static function getPaypalClientId(): string
{
$settings = new PayPalSettings();
return config('app.env') == 'local' ? $settings->sandbox_client_id : $settings->client_id;
}
/**
* @return string
*/
static function getPaypalClientSecret(): string
{
$settings = new PayPalSettings();
return config('app.env') == 'local' ? $settings->sandbox_client_secret : $settings->client_secret;
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace App\Extensions\PaymentGateways\PayPal;
use Spatie\LaravelSettings\Settings;
class PayPalSettings extends Settings
{
public bool $enabled = false;
public ?string $client_id;
public ?string $client_secret;
public ?string $sandbox_client_id;
public ?string $sandbox_client_secret;
public static function group(): string
{
return 'paypal';
}
/**
* Summary of optionInputData array
* Only used for the settings page
* @return array<array<'type'|'label'|'description'|'options', string|bool|float|int|array<string, string>>>
*/
public static function getOptionInputData()
{
return [
'category_icon' => 'fas fa-dollar-sign',
'client_id' => [
'type' => 'string',
'label' => 'Client ID',
'description' => 'The Client ID of your PayPal App',
],
'client_secret' => [
'type' => 'string',
'label' => 'Client Secret',
'description' => 'The Client Secret of your PayPal App',
],
'enabled' => [
'type' => 'boolean',
'label' => 'Enabled',
'description' => 'Enable this payment gateway',
],
'sandbox_client_id' => [
'type' => 'string',
'label' => 'Sandbox Client ID',
'description' => 'The Sandbox Client ID used when app_env = local',
],
'sandbox_client_secret' => [
'type' => 'string',
'label' => 'Sandbox Client Secret',
'description' => 'The Sandbox Client Secret used when app_env = local',
],
];
}
}

View file

@ -1,13 +0,0 @@
<?php
namespace App\Extensions\PaymentGateways\PayPal;
function getConfig()
{
return [
"name" => "PayPal",
"description" => "PayPal payment gateway",
"RoutesIgnoreCsrf" => [],
"enabled" => (config('SETTINGS::PAYMENTS:PAYPAL:SECRET') && config('SETTINGS::PAYMENTS:PAYPAL:CLIENT_ID')) || (config('SETTINGS::PAYMENTS:PAYPAL:SANDBOX_SECRET') && config('SETTINGS::PAYMENTS:PAYPAL:SANDBOX_CLIENT_ID') && env("APP_ENV") === "local"),
];
}

View file

@ -1,186 +0,0 @@
<?php
use App\Events\PaymentEvent;
use App\Events\UserUpdateCreditsEvent;
use App\Models\PartnerDiscount;
use App\Models\Payment;
use App\Models\ShopProduct;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Log;
use PayPalCheckoutSdk\Core\PayPalHttpClient;
use PayPalCheckoutSdk\Core\ProductionEnvironment;
use PayPalCheckoutSdk\Core\SandboxEnvironment;
use PayPalCheckoutSdk\Orders\OrdersCaptureRequest;
use PayPalCheckoutSdk\Orders\OrdersCreateRequest;
use PayPalHttp\HttpException;
/**
* @param Request $request
* @param ShopProduct $shopProduct
*/
function PaypalPay(Request $request)
{
/** @var User $user */
$user = Auth::user();
$shopProduct = ShopProduct::findOrFail($request->shopProduct);
$discount = PartnerDiscount::getDiscount();
// create a new payment
$payment = Payment::create([
'user_id' => $user->id,
'payment_id' => null,
'payment_method' => 'paypal',
'type' => $shopProduct->type,
'status' => 'open',
'amount' => $shopProduct->quantity,
'price' => $shopProduct->price - ($shopProduct->price * $discount / 100),
'tax_value' => $shopProduct->getTaxValue(),
'tax_percent' => $shopProduct->getTaxPercent(),
'total_price' => $shopProduct->getTotalPrice(),
'currency_code' => $shopProduct->currency_code,
'shop_item_product_id' => $shopProduct->id,
]);
$request = new OrdersCreateRequest();
$request->prefer('return=representation');
$request->body = [
"intent" => "CAPTURE",
"purchase_units" => [
[
"reference_id" => uniqid(),
"description" => $shopProduct->display . ($discount ? (" (" . __('Discount') . " " . $discount . '%)') : ""),
"amount" => [
"value" => $shopProduct->getTotalPrice(),
'currency_code' => strtoupper($shopProduct->currency_code),
'breakdown' => [
'item_total' =>
[
'currency_code' => strtoupper($shopProduct->currency_code),
'value' => $shopProduct->getPriceAfterDiscount(),
],
'tax_total' =>
[
'currency_code' => strtoupper($shopProduct->currency_code),
'value' => $shopProduct->getTaxValue(),
]
]
]
]
],
"application_context" => [
"cancel_url" => route('payment.Cancel'),
"return_url" => route('payment.PayPalSuccess', ['payment' => $payment->id]),
'brand_name' => config('app.name', 'CtrlPanel.gg'),
'shipping_preference' => 'NO_SHIPPING'
]
];
try {
// Call API with your client and get a response for your call
$response = getPayPalClient()->execute($request);
// check for any errors in the response
if ($response->statusCode != 201) {
throw new \Exception($response->statusCode);
}
// make sure the link is not empty
if (empty($response->result->links[1]->href)) {
throw new \Exception('No redirect link found');
}
Redirect::away($response->result->links[1]->href)->send();
return;
} catch (HttpException $ex) {
Log::error('PayPal Payment: ' . $ex->getMessage());
$payment->delete();
Redirect::route('store.index')->with('error', __('Payment failed'))->send();
return;
}
}
/**
* @param Request $laravelRequest
*/
function PaypalSuccess(Request $laravelRequest)
{
$user = Auth::user();
$user = User::findOrFail($user->id);
$payment = Payment::findOrFail($laravelRequest->payment);
$shopProduct = ShopProduct::findOrFail($payment->shop_item_product_id);
$request = new OrdersCaptureRequest($laravelRequest->input('token'));
$request->prefer('return=representation');
try {
// Call API with your client and get a response for your call
$response = getPayPalClient()->execute($request);
if ($response->statusCode == 201 || $response->statusCode == 200) {
//update payment
$payment->update([
'status' => 'paid',
'payment_id' => $response->result->id,
]);
event(new UserUpdateCreditsEvent($user));
event(new PaymentEvent($user, $payment, $shopProduct));
// redirect to the payment success page with success message
Redirect::route('home')->with('success', 'Payment successful')->send();
} elseif (env('APP_ENV') == 'local') {
// If call returns body in response, you can get the deserialized version from the result attribute of the response
$payment->delete();
dd($response);
} else {
$payment->update([
'status' => 'cancelled',
'payment_id' => $response->result->id,
]);
abort(500);
}
} catch (HttpException $ex) {
if (env('APP_ENV') == 'local') {
echo $ex->statusCode;
$payment->delete();
dd($ex->getMessage());
} else {
$payment->update([
'status' => 'cancelled',
'payment_id' => $response->result->id,
]);
abort(422);
}
}
}
/**
* @return PayPalHttpClient
*/
function getPayPalClient()
{
$environment = env('APP_ENV') == 'local'
? new SandboxEnvironment(getPaypalClientId(), getPaypalClientSecret())
: new ProductionEnvironment(getPaypalClientId(), getPaypalClientSecret());
return new PayPalHttpClient($environment);
}
/**
* @return string
*/
function getPaypalClientId()
{
return env('APP_ENV') == 'local' ? config("SETTINGS::PAYMENTS:PAYPAL:SANDBOX_CLIENT_ID") : config("SETTINGS::PAYMENTS:PAYPAL:CLIENT_ID");
}
/**
* @return string
*/
function getPaypalClientSecret()
{
return env('APP_ENV') == 'local' ? config("SETTINGS::PAYMENTS:PAYPAL:SANDBOX_SECRET") : config("SETTINGS::PAYMENTS:PAYPAL:SECRET");
}

View file

@ -0,0 +1,104 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
use Illuminate\Support\Facades\DB;
class CreatePayPalSettings extends SettingsMigration
{
public function up(): void
{
$table_exists = DB::table('settings_old')->exists();
$this->migrator->addEncrypted('paypal.client_id', $table_exists ? $this->getOldValue('SETTINGS::PAYMENTS:PAYPAL:CLIENT_ID') : null);
$this->migrator->addEncrypted('paypal.client_secret', $table_exists ? $this->getOldValue('SETTINGS::PAYMENTS:PAYPAL:SECRET') : null);
$this->migrator->addEncrypted('paypal.sandbox_client_id', $table_exists ? $this->getOldValue('SETTINGS::PAYMENTS:PAYPAL:SANDBOX_CLIENT_ID') : null);
$this->migrator->addEncrypted('paypal.sandbox_client_secret', $table_exists ? $this->getOldValue('SETTINGS::PAYMENTS:PAYPAL:SANDBOX_SECRET') : null);
$this->migrator->add('paypal.enabled', false);
}
public function down(): void
{
DB::table('settings_old')->insert([
[
'key' => 'SETTINGS::PAYMENTS:PAYPAL:CLIENT_ID',
'value' => $this->getNewValue('client_id'),
'type' => 'string',
'description' => 'The Client ID of your PayPal App'
],
[
'key' => 'SETTINGS::PAYMENTS:PAYPAL:SECRET',
'value' => $this->getNewValue('client_secret'),
'type' => 'string',
'description' => 'The Client Secret of your PayPal App'
],
[
'key' => 'SETTINGS::PAYMENTS:PAYPAL:SANDBOX_CLIENT_ID',
'value' => $this->getNewValue('sandbox_client_id'),
'type' => 'string',
'description' => 'The Sandbox Client ID of your PayPal App'
],
[
'key' => 'SETTINGS::PAYMENTS:PAYPAL:SANDBOX_SECRET',
'value' => $this->getNewValue('sandbox_client_secret'),
'type' => 'string',
'description' => 'The Sandbox Client Secret of your PayPal App'
]
]);
$this->migrator->delete('paypal.client_id');
$this->migrator->delete('paypal.client_secret');
$this->migrator->delete('paypal.enabled');
$this->migrator->delete('paypal.sandbox_client_id');
$this->migrator->delete('paypal.sandbox_client_secret');
}
public function getNewValue(string $name)
{
$new_value = DB::table('settings')->where([['group', '=', 'paypal'], ['name', '=', $name]])->get(['payload'])->first();
// Some keys returns '""' as a value.
if ($new_value->payload === '""') {
return null;
}
// remove the quotes from the string
if (substr($new_value->payload, 0, 1) === '"' && substr($new_value->payload, -1) === '"') {
return substr($new_value->payload, 1, -1);
}
return $new_value->payload;
}
public function getOldValue(string $key)
{
// Always get the first value of the key.
$old_value = DB::table('settings_old')->where('key', '=', $key)->get(['value', 'type'])->first();
if (is_null($old_value)) {
return null;
}
// Handle the old values to return without it being a string in all cases.
if ($old_value->type === "string" || $old_value->type === "text") {
if (is_null($old_value->value)) {
return '';
}
// Some values have the type string, but their values are boolean.
if ($old_value->value === "false" || $old_value->value === "true") {
return filter_var($old_value->value, FILTER_VALIDATE_BOOL);
}
return $old_value->value;
}
if ($old_value->type === "boolean") {
return filter_var($old_value->value, FILTER_VALIDATE_BOOL);
}
return filter_var($old_value->value, FILTER_VALIDATE_INT);
}
}

View file

@ -1,18 +1,13 @@
<?php
use Illuminate\Support\Facades\Route;
include_once(__DIR__ . '/index.php');
use App\Extensions\PaymentGateways\PayPal\PayPalExtension;
Route::middleware(['web', 'auth'])->group(function () {
Route::get('payment/PayPalPay/{shopProduct}', function () {
PaypalPay(request());
})->name('payment.PayPalPay');
Route::get(
'payment/PayPalSuccess',
function () {
PaypalSuccess(request());
PayPalExtension::PaypalSuccess(request());
}
)->name('payment.PayPalSuccess');
});

View file

@ -0,0 +1,368 @@
<?php
namespace App\Extensions\PaymentGateways\Stripe;
use App\Classes\AbstractExtension;
use App\Enums\PaymentStatus;
use App\Events\PaymentEvent;
use App\Events\CouponUsedEvent;
use App\Events\UserUpdateCreditsEvent;
use App\Extensions\PaymentGateways\Stripe\StripeSettings;
use App\Models\PartnerDiscount;
use App\Models\Payment;
use App\Models\ShopProduct;
use App\Models\User;
use App\Models\Coupon;
use App\Traits\Coupon as CouponTrait;
use App\Notifications\ConfirmPaymentNotification;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Stripe\Exception\SignatureVerificationException;
use Stripe\Stripe;
use Stripe\StripeClient;
class StripeExtension extends AbstractExtension
{
use CouponTrait;
public static function getConfig(): array
{
return [
"name" => "Stripe",
"RoutesIgnoreCsrf" => [
"payment/StripeWebhooks",
],
];
}
public static function getRedirectUrl(Payment $payment, ShopProduct $shopProduct, string $totalPriceString): string
{
// check if the total price is valid for stripe
$totalPriceNumber = floatval($totalPriceString);
if (!self::checkPriceAmount($totalPriceNumber, strtoupper($shopProduct->currency_code), 'stripe')) {
throw new Exception('Invalid price amount');
}
$stripeClient = self::getStripeClient();
$request = $stripeClient->checkout->sessions->create([
'line_items' => [
[
'price_data' => [
'currency' => $shopProduct->currency_code,
'product_data' => [
'name' => $shopProduct->display,
'description' => $shopProduct->description,
],
'unit_amount_decimal' => $totalPriceString,
],
'quantity' => 1,
],
[
'price_data' => [
'currency' => $shopProduct->currency_code,
'product_data' => [
'name' => __('Tax'),
'description' => $shopProduct->getTaxPercent() . '%',
],
'unit_amount_decimal' => round($shopProduct->getTaxValue(), 2) * 100,
],
'quantity' => 1,
],
],
'mode' => 'payment',
'success_url' => route('payment.StripeSuccess', ['payment' => $payment->id]) . '&session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('payment.Cancel'),
'payment_intent_data' => [
'metadata' => [
'payment_id' => $payment->id,
],
],
]);
return $request->url;
}
/**
* @param Request $request
*/
public static function StripeSuccess(Request $request)
{
$user = Auth::user();
$user = User::findOrFail($user->id);
$payment = Payment::findOrFail($request->input('payment'));
$shopProduct = ShopProduct::findOrFail($payment->shop_item_product_id);
Redirect::route('home')->with('success', 'Please wait for success')->send();
$stripeClient = self::getStripeClient();
try {
//get stripe data
$paymentSession = $stripeClient->checkout->sessions->retrieve($request->input('session_id'));
$paymentIntent = $stripeClient->paymentIntents->retrieve($paymentSession->payment_intent);
//get DB entry of this payment ID if existing
$paymentDbEntry = Payment::where('payment_id', $paymentSession->payment_intent)->count();
// check if payment is 100% completed and payment does not exist in db already
if ($paymentSession->status == 'complete' && $paymentIntent->status == 'succeeded' && $paymentDbEntry == 0) {
//update payment
$payment->update([
'payment_id' => $paymentSession->payment_intent,
'status' => PaymentStatus::PAID,
]);
//payment notification
$user->notify(new ConfirmPaymentNotification($payment));
event(new UserUpdateCreditsEvent($user));
event(new PaymentEvent($user, $payment, $shopProduct));
//redirect back to home
Redirect::route('home')->with('success', 'Payment successful')->send();
} else {
if ($paymentIntent->status == 'processing') {
//update payment
$payment->update([
'payment_id' => $paymentSession->payment_intent,
'status' => PaymentStatus::PROCESSING,
]);
event(new PaymentEvent($user, $payment, $shopProduct));
Redirect::route('home')->with('success', 'Your payment is being processed')->send();
}
if ($paymentDbEntry == 0 && $paymentIntent->status != 'processing') {
$stripeClient->paymentIntents->cancel($paymentIntent->id);
//redirect back to home
Redirect::route('home')->with('info', __('Your payment has been canceled!'))->send();
} else {
abort(402);
}
}
} catch (Exception $e) {
if (env('APP_ENV') == 'local') {
dd($e->getMessage());
} else {
abort(422);
}
}
}
/**
* @param Request $request
*/
public static function handleStripePaymentSuccessHook($paymentIntent)
{
try {
$payment = Payment::where('id', $paymentIntent->metadata->payment_id)->with('user')->first();
$user = User::where('id', $payment->user_id)->first();
$shopProduct = ShopProduct::findOrFail($payment->shop_item_product_id);
if ($paymentIntent->status == 'succeeded' && $payment->status == 'processing') {
//update payment db entry status
$payment->update([
'payment_id' => $payment->payment_id ?? $paymentIntent->id,
'status' => PaymentStatus::PAID,
]);
//payment notification
$user->notify(new ConfirmPaymentNotification($payment));
event(new UserUpdateCreditsEvent($user));
event(new PaymentEvent($user, $payment, $shopProduct));
}
// return 200
return response()->json(['success' => true], 200);
} catch (Exception $ex) {
abort(422);
}
}
/**
* @param Request $request
*/
public static function StripeWebhooks(Request $request)
{
Stripe::setApiKey(self::getStripeSecret());
try {
$payload = @file_get_contents('php://input');
$sig_header = $request->header('Stripe-Signature');
$event = null;
$event = \Stripe\Webhook::constructEvent(
$payload,
$sig_header,
self::getStripeEndpointSecret()
);
} catch (\UnexpectedValueException $e) {
// Invalid payload
abort(400);
} catch (SignatureVerificationException $e) {
// Invalid signature
abort(400);
}
// Handle the event
switch ($event->type) {
case 'payment_intent.succeeded':
$paymentIntent = $event->data->object; // contains a \Stripe\PaymentIntent
self::handleStripePaymentSuccessHook($paymentIntent);
break;
default:
echo 'Received unknown event type ' . $event->type;
}
}
/**
* @return \Stripe\StripeClient
*/
public static function getStripeClient()
{
return new StripeClient(self::getStripeSecret());
}
/**
* @return string
*/
public static function getStripeSecret()
{
$settings = new StripeSettings();
return env('APP_ENV') == 'local'
? $settings->test_secret_key
: $settings->secret_key;
}
/**
* @return string
*/
public static function getStripeEndpointSecret()
{
$settings = new StripeSettings();
return env('APP_ENV') == 'local'
? $settings->test_endpoint_secret
: $settings->endpoint_secret;
}
/**
* @param $amount
* @param $currencyCode
* @param $payment_method
* @return bool
* @description check if the amount is higher than the minimum amount for the stripe gateway
*/
public static function checkPriceAmount(float $amount, string $currencyCode, string $payment_method)
{
$minimums = [
"USD" => [
"paypal" => 0,
"stripe" => 0.5
],
"AED" => [
"paypal" => 0,
"stripe" => 2
],
"AUD" => [
"paypal" => 0,
"stripe" => 0.5
],
"BGN" => [
"paypal" => 0,
"stripe" => 1
],
"BRL" => [
"paypal" => 0,
"stripe" => 0.5
],
"CAD" => [
"paypal" => 0,
"stripe" => 0.5
],
"CHF" => [
"paypal" => 0,
"stripe" => 0.5
],
"CZK" => [
"paypal" => 0,
"stripe" => 15
],
"DKK" => [
"paypal" => 0,
"stripe" => 2.5
],
"EUR" => [
"paypal" => 0,
"stripe" => 0.5
],
"GBP" => [
"paypal" => 0,
"stripe" => 0.3
],
"HKD" => [
"paypal" => 0,
"stripe" => 4
],
"HRK" => [
"paypal" => 0,
"stripe" => 0.5
],
"HUF" => [
"paypal" => 0,
"stripe" => 175
],
"INR" => [
"paypal" => 0,
"stripe" => 0.5
],
"JPY" => [
"paypal" => 0,
"stripe" => 0.5
],
"MXN" => [
"paypal" => 0,
"stripe" => 10
],
"MYR" => [
"paypal" => 0,
"stripe" => 2
],
"NOK" => [
"paypal" => 0,
"stripe" => 3
],
"NZD" => [
"paypal" => 0,
"stripe" => 0.5
],
"PLN" => [
"paypal" => 0,
"stripe" => 2
],
"RON" => [
"paypal" => 0,
"stripe" => 2
],
"SEK" => [
"paypal" => 0,
"stripe" => 3
],
"SGD" => [
"paypal" => 0,
"stripe" => 0.5
],
"THB" => [
"paypal" => 0,
"stripe" => 10
]
];
return $amount >= $minimums[$currencyCode][$payment_method];
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace App\Extensions\PaymentGateways\Stripe;
use Spatie\LaravelSettings\Settings;
class StripeSettings extends Settings
{
public bool $enabled = false;
public ?string $secret_key;
public ?string $endpoint_secret;
public ?string $test_secret_key;
public ?string $test_endpoint_secret;
public static function group(): string
{
return 'stripe';
}
public static function getOptionInputData()
{
return [
'category_icon' => 'fas fa-dollar-sign',
'secret_key' => [
'type' => 'string',
'label' => 'Secret Key',
'description' => 'The Secret Key of your Stripe App',
],
'endpoint_secret' => [
'type' => 'string',
'label' => 'Endpoint Secret',
'description' => 'The Endpoint Secret of your Stripe App',
],
'test_secret_key' => [
'type' => 'string',
'label' => 'Test Secret Key',
'description' => 'The Test Secret Key used when app_env = local',
],
'test_endpoint_secret' => [
'type' => 'string',
'label' => 'Test Endpoint Secret',
'description' => 'The Test Endpoint Secret used when app_env = local',
],
'enabled' => [
'type' => 'boolean',
'label' => 'Enabled',
'description' => 'Enable this payment gateway',
]
];
}
}

View file

@ -1,15 +0,0 @@
<?php
namespace App\Extensions\PaymentGateways\Stripe;
function getConfig()
{
return [
"name" => "Stripe",
"description" => "Stripe payment gateway",
"RoutesIgnoreCsrf" => [
"payment/StripeWebhooks",
],
"enabled" => config('SETTINGS::PAYMENTS:STRIPE:SECRET') && config('SETTINGS::PAYMENTS:STRIPE:ENDPOINT_SECRET') || config('SETTINGS::PAYMENTS:STRIPE:ENDPOINT_TEST_SECRET') && config('SETTINGS::PAYMENTS:STRIPE:TEST_SECRET') && env("APP_ENV") === "local",
];
}

View file

@ -1,373 +0,0 @@
<?php
use App\Events\PaymentEvent;
use App\Events\UserUpdateCreditsEvent;
use App\Models\PartnerDiscount;
use App\Models\Payment;
use App\Models\ShopProduct;
use App\Models\User;
use App\Notifications\ConfirmPaymentNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Stripe\Exception\SignatureVerificationException;
use Stripe\Stripe;
use Stripe\StripeClient;
/**
* @param Request $request
* @param ShopProduct $shopProduct
*/
function StripePay(Request $request)
{
$user = Auth::user();
$shopProduct = ShopProduct::findOrFail($request->shopProduct);
// check if the price is valid for stripe
if (!checkPriceAmount($shopProduct->getTotalPrice(), strtoupper($shopProduct->currency_code), 'stripe')) {
Redirect::route('home')->with('error', __('The product you chose can\'t be purchased with this payment method. The total amount is too small. Please buy a bigger amount or try a different payment method.'))->send();
return;
}
$discount = PartnerDiscount::getDiscount();
// create payment
$payment = Payment::create([
'user_id' => $user->id,
'payment_id' => null,
'payment_method' => 'stripe',
'type' => $shopProduct->type,
'status' => 'open',
'amount' => $shopProduct->quantity,
'price' => $shopProduct->price - ($shopProduct->price * $discount / 100),
'tax_value' => $shopProduct->getTaxValue(),
'total_price' => $shopProduct->getTotalPrice(),
'tax_percent' => $shopProduct->getTaxPercent(),
'currency_code' => $shopProduct->currency_code,
'shop_item_product_id' => $shopProduct->id,
]);
$stripeClient = getStripeClient();
$request = $stripeClient->checkout->sessions->create([
'line_items' => [
[
'price_data' => [
'currency' => $shopProduct->currency_code,
'product_data' => [
'name' => $shopProduct->display . ($discount ? (' (' . __('Discount') . ' ' . $discount . '%)') : ''),
'description' => $shopProduct->description,
],
'unit_amount_decimal' => round($shopProduct->getPriceAfterDiscount() * 100, 2),
],
'quantity' => 1,
],
[
'price_data' => [
'currency' => $shopProduct->currency_code,
'product_data' => [
'name' => __('Tax'),
'description' => $shopProduct->getTaxPercent() . '%',
],
'unit_amount_decimal' => round($shopProduct->getTaxValue(), 2) * 100,
],
'quantity' => 1,
],
],
'mode' => 'payment',
'payment_method_types' => str_getcsv(config('SETTINGS::PAYMENTS:STRIPE:METHODS')),
'success_url' => route('payment.StripeSuccess', ['payment' => $payment->id]) . '&session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('payment.Cancel'),
'payment_intent_data' => [
'metadata' => [
'payment_id' => $payment->id,
],
],
]);
Redirect::to($request->url)->send();
}
/**
* @param Request $request
*/
function StripeSuccess(Request $request)
{
$user = Auth::user();
$user = User::findOrFail($user->id);
$payment = Payment::findOrFail($request->input('payment'));
$shopProduct = ShopProduct::findOrFail($payment->shop_item_product_id);
Redirect::route('home')->with('success', 'Please wait for success')->send();
$stripeClient = getStripeClient();
try {
//get stripe data
$paymentSession = $stripeClient->checkout->sessions->retrieve($request->input('session_id'));
$paymentIntent = $stripeClient->paymentIntents->retrieve($paymentSession->payment_intent);
//get DB entry of this payment ID if existing
$paymentDbEntry = Payment::where('payment_id', $paymentSession->payment_intent)->count();
// check if payment is 100% completed and payment does not exist in db already
if ($paymentSession->status == 'complete' && $paymentIntent->status == 'succeeded' && $paymentDbEntry == 0) {
//update payment
$payment->update([
'payment_id' => $paymentSession->payment_intent,
'status' => 'paid',
]);
//payment notification
$user->notify(new ConfirmPaymentNotification($payment));
event(new UserUpdateCreditsEvent($user));
event(new PaymentEvent($user, $payment, $shopProduct));
//redirect back to home
Redirect::route('home')->with('success', 'Payment successful')->send();
} else {
if ($paymentIntent->status == 'processing') {
//update payment
$payment->update([
'payment_id' => $paymentSession->payment_intent,
'status' => 'processing',
]);
event(new PaymentEvent($user, $payment, $shopProduct));
Redirect::route('home')->with('success', 'Your payment is being processed')->send();
}
if ($paymentDbEntry == 0 && $paymentIntent->status != 'processing') {
$stripeClient->paymentIntents->cancel($paymentIntent->id);
//redirect back to home
Redirect::route('home')->with('info', __('Your payment has been canceled!'))->send();
} else {
abort(402);
}
}
} catch (Exception $e) {
if (env('APP_ENV') == 'local') {
dd($e->getMessage());
} else {
abort(422);
}
}
}
/**
* @param Request $request
*/
function handleStripePaymentSuccessHook($paymentIntent)
{
try {
$payment = Payment::where('id', $paymentIntent->metadata->payment_id)->with('user')->first();
$user = User::where('id', $payment->user_id)->first();
$shopProduct = ShopProduct::findOrFail($payment->shop_item_product_id);
if ($paymentIntent->status == 'succeeded' && $payment->status == 'processing') {
//update payment db entry status
$payment->update([
'payment_id' => $payment->payment_id ?? $paymentIntent->id,
'status' => 'paid'
]);
//payment notification
$user->notify(new ConfirmPaymentNotification($payment));
event(new UserUpdateCreditsEvent($user));
event(new PaymentEvent($user, $payment, $shopProduct));
}
// return 200
return response()->json(['success' => true], 200);
} catch (Exception $ex) {
abort(422);
}
}
/**
* @param Request $request
*/
function StripeWebhooks(Request $request)
{
Stripe::setApiKey(getStripeSecret());
try {
$payload = @file_get_contents('php://input');
$sig_header = $request->header('Stripe-Signature');
$event = null;
$event = \Stripe\Webhook::constructEvent(
$payload,
$sig_header,
getStripeEndpointSecret()
);
} catch (\UnexpectedValueException $e) {
// Invalid payload
abort(400);
} catch (SignatureVerificationException $e) {
// Invalid signature
abort(400);
}
// Handle the event
switch ($event->type) {
case 'payment_intent.succeeded':
$paymentIntent = $event->data->object; // contains a \Stripe\PaymentIntent
handleStripePaymentSuccessHook($paymentIntent);
break;
default:
echo 'Received unknown event type ' . $event->type;
}
}
/**
* @return \Stripe\StripeClient
*/
function getStripeClient()
{
return new StripeClient(getStripeSecret());
}
/**
* @return string
*/
function getStripeSecret()
{
return env('APP_ENV') == 'local'
? config('SETTINGS::PAYMENTS:STRIPE:TEST_SECRET')
: config('SETTINGS::PAYMENTS:STRIPE:SECRET');
}
/**
* @return string
*/
function getStripeEndpointSecret()
{
return env('APP_ENV') == 'local'
? config('SETTINGS::PAYMENTS:STRIPE:ENDPOINT_TEST_SECRET')
: config('SETTINGS::PAYMENTS:STRIPE:ENDPOINT_SECRET');
}
/**
* @param $amount
* @param $currencyCode
* @param $payment_method
* @return bool
* @description check if the amount is higher than the minimum amount for the stripe gateway
*/
function checkPriceAmount($amount, $currencyCode, $payment_method)
{
$minimums = [
"USD" => [
"paypal" => 0,
"stripe" => 0.5
],
"AED" => [
"paypal" => 0,
"stripe" => 2
],
"AUD" => [
"paypal" => 0,
"stripe" => 0.5
],
"BGN" => [
"paypal" => 0,
"stripe" => 1
],
"BRL" => [
"paypal" => 0,
"stripe" => 0.5
],
"CAD" => [
"paypal" => 0,
"stripe" => 0.5
],
"CHF" => [
"paypal" => 0,
"stripe" => 0.5
],
"CZK" => [
"paypal" => 0,
"stripe" => 15
],
"DKK" => [
"paypal" => 0,
"stripe" => 2.5
],
"EUR" => [
"paypal" => 0,
"stripe" => 0.5
],
"GBP" => [
"paypal" => 0,
"stripe" => 0.3
],
"HKD" => [
"paypal" => 0,
"stripe" => 4
],
"HRK" => [
"paypal" => 0,
"stripe" => 0.5
],
"HUF" => [
"paypal" => 0,
"stripe" => 175
],
"INR" => [
"paypal" => 0,
"stripe" => 0.5
],
"JPY" => [
"paypal" => 0,
"stripe" => 0.5
],
"MXN" => [
"paypal" => 0,
"stripe" => 10
],
"MYR" => [
"paypal" => 0,
"stripe" => 2
],
"NOK" => [
"paypal" => 0,
"stripe" => 3
],
"NZD" => [
"paypal" => 0,
"stripe" => 0.5
],
"PLN" => [
"paypal" => 0,
"stripe" => 2
],
"RON" => [
"paypal" => 0,
"stripe" => 2
],
"SEK" => [
"paypal" => 0,
"stripe" => 3
],
"SGD" => [
"paypal" => 0,
"stripe" => 0.5
],
"THB" => [
"paypal" => 0,
"stripe" => 10
]
];
return $amount >= $minimums[$currencyCode][$payment_method];
}

View file

@ -0,0 +1,102 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
use Illuminate\Support\Facades\DB;
class CreateStripeSettings extends SettingsMigration
{
public function up(): void
{
$table_exists = DB::table('settings_old')->exists();
$this->migrator->addEncrypted('stripe.secret_key', $table_exists ? $this->getOldValue('SETTINGS::PAYMENTS:STRIPE:SECRET') : null);
$this->migrator->add('stripe.endpoint_secret', $table_exists ? $this->getOldValue('SETTINGS::PAYMENTS:STRIPE:ENDPOINT_SECRET') : null);
$this->migrator->addEncrypted('stripe.test_secret_key', $table_exists ? $this->getOldValue('SETTINGS::PAYMENTS:STRIPE:TEST_SECRET') : null);
$this->migrator->add('stripe.test_endpoint_secret', $table_exists ? $this->getOldValue('SETTINGS::PAYMENTS:STRIPE:ENDPOINT_TEST_SECRET') : null);
$this->migrator->add('stripe.enabled', false);
}
public function down(): void
{
DB::table('settings_old')->insert([
[
'key' => 'SETTINGS::PAYMENTS:STRIPE:SECRET',
'value' => $this->getNewValue('secret_key'),
'type' => 'string',
'description' => 'The Secret Key of your Stripe App'
],
[
'key' => 'SETTINGS::PAYMENTS:STRIPE:ENDPOINT_SECRET',
'value' => $this->getNewValue('endpoint_secret'),
'type' => 'string',
'description' => 'The Endpoint Secret of your Stripe App'
],
[
'key' => 'SETTINGS::PAYMENTS:STRIPE:TEST_SECRET',
'value' => $this->getNewValue('test_secret_key'),
'type' => 'string',
'description' => 'The Test Secret Key of your Stripe App'
],
[
'key' => 'SETTINGS::PAYMENTS:STRIPE:ENDPOINT_TEST_SECRET',
'value' => $this->getNewValue('test_endpoint_secret'),
'type' => 'string',
'description' => 'The Test Endpoint Secret of your Stripe App'
]
]);
$this->migrator->delete('stripe.secret_key');
$this->migrator->delete('stripe.endpoint_secret');
$this->migrator->delete('stripe.enabled');
$this->migrator->delete('stripe.test_secret_key');
$this->migrator->delete('stripe.test_endpoint_secret');
}
public function getNewValue(string $name)
{
$new_value = DB::table('settings')->where([['group', '=', 'stripe'], ['name', '=', $name]])->get(['payload'])->first();
// Some keys returns '""' as a value.
if ($new_value->payload === '""') {
return null;
}
// remove the quotes from the string
if (substr($new_value->payload, 0, 1) === '"' && substr($new_value->payload, -1) === '"') {
return substr($new_value->payload, 1, -1);
}
return $new_value->payload;
}
public function getOldValue(string $key)
{
// Always get the first value of the key.
$old_value = DB::table('settings_old')->where('key', '=', $key)->get(['value', 'type'])->first();
if (is_null($old_value)) {
return null;
}
// Handle the old values to return without it being a string in all cases.
if ($old_value->type === "string" || $old_value->type === "text") {
if (is_null($old_value->value)) {
return '';
}
// Some values have the type string, but their values are boolean.
if ($old_value->value === "false" || $old_value->value === "true") {
return filter_var($old_value->value, FILTER_VALIDATE_BOOL);
}
return $old_value->value;
}
if ($old_value->type === "boolean") {
return filter_var($old_value->value, FILTER_VALIDATE_BOOL);
}
return filter_var($old_value->value, FILTER_VALIDATE_INT);
}
}

View file

@ -1,17 +1,13 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Extensions\PaymentGateways\Stripe\StripeExtension;
include_once(__DIR__ . '/index.php');
Route::middleware(['web', 'auth'])->group(function () {
Route::get('payment/StripePay/{shopProduct}', function () {
StripePay(request());
})->name('payment.StripePay');
Route::get(
'payment/StripeSuccess',
function () {
StripeSuccess(request());
StripeExtension::StripeSuccess(request());
}
)->name('payment.StripeSuccess');
});
@ -19,5 +15,5 @@ Route::middleware(['web', 'auth'])->group(function () {
// Stripe WebhookRoute -> validation in Route Handler
Route::post('payment/StripeWebhooks', function () {
StripeWebhooks(request());
StripeExtension::StripeWebhooks(request());
})->name('payment.StripeWebhooks');

View file

@ -2,65 +2,14 @@
namespace App\Helpers;
/**
* Summary of ExtensionHelper
*/
class ExtensionHelper
{
/**
* Get a config of an extension by its name
* @param string $extensionName
* @param string $configname
*/
public static function getExtensionConfig(string $extensionName, string $configname)
{
$extensions = ExtensionHelper::getAllExtensions();
// call the getConfig function of the config file of the extension like that
// call_user_func("App\\Extensions\\PaymentGateways\\Stripe" . "\\getConfig");
foreach ($extensions as $extension) {
if (!(basename($extension) == $extensionName)) {
continue;
}
$configFile = $extension . '/config.php';
if (file_exists($configFile)) {
include_once $configFile;
$config = call_user_func('App\\Extensions\\' . basename(dirname($extension)) . '\\' . basename($extension) . "\\getConfig");
}
if (isset($config[$configname])) {
return $config[$configname];
}
}
return null;
}
public static function getAllCsrfIgnoredRoutes()
{
$extensions = ExtensionHelper::getAllExtensions();
$routes = [];
foreach ($extensions as $extension) {
$configFile = $extension . '/config.php';
if (file_exists($configFile)) {
include_once $configFile;
$config = call_user_func('App\\Extensions\\' . basename(dirname($extension)) . '\\' . basename($extension) . "\\getConfig");
}
if (isset($config['RoutesIgnoreCsrf'])) {
$routes = array_merge($routes, $config['RoutesIgnoreCsrf']);
}
// map over the routes and add the extension name as prefix
$result = array_map(fn ($item) => "extensions/{$item}", $routes);
}
return $result;
}
/**
* Get all extensions
* @return array
* @return array array of all extensions e.g. ["App\Extensions\PayPal", "App\Extensions\Stripe"]
*/
public static function getAllExtensions()
{
@ -70,13 +19,207 @@ class ExtensionHelper
$extensions = array_merge($extensions, glob($extensionNamespace . '/*', GLOB_ONLYDIR));
}
// remove base path from every extension but keep app/Extensions/...
$extensions = array_map(fn ($item) => str_replace(app_path() . '/', 'App/', $item), $extensions);
return $extensions;
}
/**
* Get all extensions by namespace
* @param string $namespace case sensitive namespace of the extension e.g. PaymentGateways
* @return array array of all extensions e.g. ["App\Extensions\PayPal", "App\Extensions\Stripe"]
*/
public static function getAllExtensionsByNamespace(string $namespace)
{
$extensions = glob(app_path() . '/Extensions/' . $namespace . '/*', GLOB_ONLYDIR);
// remove base path from every extension but keep app/Extensions/...
$extensions = array_map(fn ($item) => str_replace(app_path() . '/', 'App/', $item), $extensions);
return $extensions;
}
/**
* Get an extension by its name
* @param string $extensionName case sensitive name of the extension e.g. PayPal
* @return string|null the path of the extension e.g. App\Extensions\PayPal
*/
public static function getExtension(string $extensionName)
{
$extensions = self::getAllExtensions();
// filter the extensions by the extension name
$extensions = array_filter($extensions, fn ($item) => basename($item) == $extensionName);
// return the only extension
return array_shift($extensions);
}
/**
* Get all extension classes
* @return array array of all extension classes e.g. ["App\Extensions\PayPal\PayPalExtension", "App\Extensions\Stripe\StripeExtension"]
*/
public static function getAllExtensionClasses()
{
$extensions = self::getAllExtensions();
// replace all slashes with backslashes
$extensions = array_map(fn ($item) => str_replace('/', '\\', $item), $extensions);
// add the ExtensionClass to the end of the namespace
$extensions = array_map(fn ($item) => $item . '\\' . basename($item) . 'Extension', $extensions);
// filter out non existing extension classes
$extensions = array_filter($extensions, fn ($item) => class_exists($item));
return $extensions;
}
/**
* Get all extension classes by namespace
* @param string $namespace case sensitive namespace of the extension e.g. PaymentGateways
* @return array array of all extension classes e.g. ["App\Extensions\PayPal\PayPalExtension", "App\Extensions\Stripe\StripeExtension"]
*/
public static function getAllExtensionClassesByNamespace(string $namespace)
{
$extensions = self::getAllExtensionsByNamespace($namespace);
// replace all slashes with backslashes
$extensions = array_map(fn ($item) => str_replace('/', '\\', $item), $extensions);
// add the ExtensionClass to the end of the namespace
$extensions = array_map(fn ($item) => $item . '\\' . basename($item) . 'Extension', $extensions);
// filter out non existing extension classes
$extensions = array_filter($extensions, fn ($item) => class_exists($item));
return $extensions;
}
/**
* Get the class of an extension by its name
* @param string $extensionName case sensitive name of the extension e.g. PayPal
* @return string|null the class name of the extension e.g. App\Extensions\PayPal\PayPalExtension
*/
public static function getExtensionClass(string $extensionName)
{
$extensions = self::getAllExtensions();
foreach ($extensions as $extension) {
if (!(basename($extension) == $extensionName)) {
continue;
}
$extension = str_replace('/', '\\', $extension);
$extensionClass = $extension . '\\' . $extensionName . 'Extension';
return $extensionClass;
}
}
/**
* Get a config of an extension by its name
* @param string $extensionName
* @param string $configname
*/
public static function getExtensionConfig(string $extensionName, string $configname)
{
$extension = self::getExtensionClass($extensionName);
$config = $extension::getConfig();
if (isset($config[$configname])) {
return $config[$configname];
}
return null;
}
public static function getAllCsrfIgnoredRoutes()
{
$extensions = self::getAllExtensionClasses();
$routes = [];
foreach ($extensions as $extension) {
$config = $extension::getConfig();
if (isset($config['RoutesIgnoreCsrf'])) {
$routes = array_merge($routes, $config['RoutesIgnoreCsrf']);
}
}
// map over the routes and add the extension name as prefix
$result = array_map(fn ($item) => "extensions/{$item}", $routes);
return $result;
}
/**
* Summary of getAllExtensionMigrations
* @return array of all migration paths look like: app/Extensions/ExtensionNamespace/ExtensionName/migrations/
*/
public static function getAllExtensionMigrations()
{
$extensions = self::getAllExtensions();
// Transform the extensions to a path
$extensions = array_map(fn ($item) => self::extensionNameToPath($item), $extensions);
// get all migration directories of the extensions and return them as array
$migrations = [];
foreach ($extensions as $extension) {
$migrationDir = $extension . '/migrations';
if (file_exists($migrationDir)) {
$migrations[] = $migrationDir;
}
}
return $migrations;
}
/**
* Summary of getAllExtensionSettings
* @return array of all setting classes look like: App\Extensions\PaymentGateways\PayPal\PayPalSettings
*/
public static function getAllExtensionSettingsClasses()
{
$extensions = self::getAllExtensions();
$settings = [];
foreach ($extensions as $extension) {
$extensionName = basename($extension);
// replace all slashes with backslashes
$extension = str_replace('/', '\\', $extension);
$settingsClass = $extension . '\\' . $extensionName . 'Settings';
if (class_exists($settingsClass)) {
$settings[] = $settingsClass;
}
}
return $settings;
}
public static function getExtensionSettings(string $extensionName)
{
$extension = self::getExtension($extensionName);
// replace all slashes with backslashes
$extension = str_replace('/', '\\', $extension);
$settingClass = $extension . '\\' . $extensionName . 'Settings';
if (class_exists($settingClass)) {
return new $settingClass();
}
}
/**
* Transforms a extension name to a path
* @param string $extensionName e.g. App\Extensions\PaymentGateways\PayPal
* @return string e.g. C:\xampp\htdocs\laravel\app/Extensions/PaymentGateways/PayPal
*/
private static function extensionNameToPath(string $extensionName)
{
return app_path() . '/' . str_replace('App/', '', $extensionName);
}
}

View file

@ -14,6 +14,7 @@ use Spatie\Activitylog\Models\Activity;
class ActivityLogController extends Controller
{
const VIEW_PERMISSION = "admin.logs.read";
/**
* Display a listing of the resource.
*
@ -21,6 +22,9 @@ class ActivityLogController extends Controller
*/
public function index(Request $request)
{
$this->checkPermission(self::VIEW_PERMISSION);
$cronLogs = Storage::disk('logs')->exists('cron.log') ? Storage::disk('logs')->get('cron.log') : null;
if ($request->input('search')) {

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ApplicationApi;
use App\Settings\LocaleSettings;
use Exception;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
@ -15,14 +16,20 @@ use Illuminate\Http\Response;
class ApplicationApiController extends Controller
{
const READ_PERMISSION = "admin.api.read";
const WRITE_PERMISSION = "admin.api.write";
/**
* Display a listing of the resource.
*
* @return Application|Factory|View|Response
*/
public function index()
public function index(LocaleSettings $locale_settings)
{
return view('admin.api.index');
$this->checkPermission(self::READ_PERMISSION);
return view('admin.api.index', [
'locale_datatables' => $locale_settings->datatables
]);
}
/**
@ -32,6 +39,8 @@ class ApplicationApiController extends Controller
*/
public function create()
{
$this->checkPermission(self::WRITE_PERMISSION);
return view('admin.api.create');
}
@ -73,6 +82,7 @@ class ApplicationApiController extends Controller
*/
public function edit(ApplicationApi $applicationApi)
{
$this->checkPermission(self::WRITE_PERMISSION);
return view('admin.api.edit', [
'applicationApi' => $applicationApi,
]);
@ -104,6 +114,8 @@ class ApplicationApiController extends Controller
*/
public function destroy(ApplicationApi $applicationApi)
{
$this->checkPermission(self::WRITE_PERMISSION);
$applicationApi->delete();
return redirect()->back()->with('success', __('api key has been removed!'));

View file

@ -0,0 +1,242 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Coupon;
use App\Settings\LocaleSettings;
use App\Traits\Coupon as CouponTrait;
use Illuminate\Http\Request;
use Carbon\Carbon;
class CouponController extends Controller
{
const READ_PERMISSION = "admin.coupons.read";
const WRITE_PERMISSION = "admin.coupons.write";
use CouponTrait;
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(LocaleSettings $localeSettings)
{
$this->checkPermission(self::READ_PERMISSION);
return view('admin.coupons.index', [
'locale_datatables' => $localeSettings->datatables
]);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
$this->checkPermission(self::WRITE_PERMISSION);
return view('admin.coupons.create');
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$coupon_code = $request->input('code');
$random_codes_amount = $request->input('range_codes');
$rules = $this->requestRules($request);
// If for some reason you pass both fields at once.
if ($coupon_code && $random_codes_amount) {
return redirect()->back()->with('error', __('Only one of the two code inputs must be provided.'))->withInput($request->all());
}
if (!$coupon_code && !$random_codes_amount) {
return redirect()->back()->with('error', __('At least one of the two code inputs must be provided.'))->withInput($request->all());
}
$request->validate($rules);
if (array_key_exists('range_codes', $rules)) {
$data = [];
$coupons = Coupon::generateRandomCoupon($random_codes_amount);
// Scroll through all the randomly generated coupons.
foreach ($coupons as $coupon) {
$data[] = [
'code' => $coupon,
'type' => $request->input('type'),
'value' => $request->input('value'),
'max_uses' => $request->input('max_uses'),
'expires_at' => $request->input('expires_at'),
'created_at' => Carbon::now(), // Does not fill in by itself when using the 'insert' method.
'updated_at' => Carbon::now()
];
}
Coupon::insert($data);
} else {
Coupon::create($request->except('_token'));
}
return redirect()->route('admin.coupons.index')->with('success', __("The coupon's was registered successfully."));
}
/**
* Display the specified resource.
*
* @param \App\Models\Coupon $coupon
* @return \Illuminate\Http\Response
*/
public function show(Coupon $coupon)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Coupon $coupon
* @return \Illuminate\Http\Response
*/
public function edit(Coupon $coupon)
{
$this->checkPermission(self::WRITE_PERMISSION);
return view('admin.coupons.edit', [
'coupon' => $coupon,
'expired_at' => $coupon->expires_at ? Carbon::createFromTimestamp($coupon->expires_at) : null
]);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Coupon $coupon
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Coupon $coupon)
{
$coupon_code = $request->input('code');
$random_codes_amount = $request->input('range_codes');
$rules = $this->requestRules($request);
// If for some reason you pass both fields at once.
if ($coupon_code && $random_codes_amount) {
return redirect()->back()->with('error', __('Only one of the two code inputs must be provided.'))->withInput($request->all());
}
if (!$coupon_code && !$random_codes_amount) {
return redirect()->back()->with('error', __('At least one of the two code inputs must be provided.'))->withInput($request->all());
}
$request->validate($rules);
$coupon->update($request->except('_token'));
return redirect()->route('admin.coupons.index')->with('success', __('coupon has been updated!'));
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Coupon $coupon
* @return \Illuminate\Http\Response
*/
public function destroy(Coupon $coupon)
{
$this->checkPermission(self::WRITE_PERMISSION);
$coupon->delete();
return redirect()->back()->with('success', __('coupon has been removed!'));
}
private function requestRules(Request $request)
{
$coupon_code = $request->input('code');
$random_codes_amount = $request->input('range_codes');
$rules = [
"type" => "required|string|in:percentage,amount",
"max_uses" => "required|integer|digits_between:1,100",
"value" => "required|numeric|between:0,100",
"expires_at" => "nullable|date|after:" . Carbon::now()->format(Coupon::formatDate())
];
if ($coupon_code) {
$rules['code'] = "required|string|min:4";
} elseif ($random_codes_amount) {
$rules['range_codes'] = 'required|integer|digits_between:1,100';
}
return $rules;
}
public function redeem(Request $request)
{
return $this->validateCoupon($request->user(), $request->input('couponCode'), $request->input('productId'));
}
public function dataTable()
{
$query = Coupon::selectRaw('
coupons.*,
CASE
WHEN coupons.uses >= coupons.max_uses THEN "USES_LIMIT_REACHED"
WHEN coupons.expires_at IS NOT NULL AND coupons.expires_at < NOW() THEN "EXPIRED"
ELSE "VALID"
END as derived_status
');
return datatables($query)
->addColumn('actions', function(Coupon $coupon) {
return '
<a data-content="'.__('Edit').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('admin.coupons.edit', $coupon->id).'" class="mr-1 btn btn-sm btn-info"><i class="fas fa-pen"></i></a>
<form class="d-inline" onsubmit="return submitResult();" method="post" action="'.route('admin.coupons.destroy', $coupon->id).'">
'.csrf_field().'
'.method_field('DELETE').'
<button data-content="'.__('Delete').'" data-toggle="popover" data-trigger="hover" data-placement="top" class="mr-1 btn btn-sm btn-danger"><i class="fas fa-trash"></i></button>
</form>
';
})
->addColumn('status', function (Coupon $coupon) {
$color = ($coupon->derived_status == 'VALID') ? 'success' : 'danger';
$status = str_replace('_', ' ', $coupon->derived_status);
return '<span class="badge badge-'.$color.'">'.$status.'</span>';
})
->editColumn('uses', function (Coupon $coupon) {
return "{$coupon->uses} / {$coupon->max_uses}";
})
->editColumn('value', function (Coupon $coupon) {
if ($coupon->type === 'percentage') {
return $coupon->value . "%";
}
return number_format($coupon->value, 2, '.', '');
})
->editColumn('expires_at', function (Coupon $coupon) {
if (!$coupon->expires_at) {
return __('Never');
}
return Carbon::createFromTimestamp($coupon->expires_at);
})
->editColumn('created_at', function(Coupon $coupon) {
return Carbon::createFromTimeString($coupon->created_at);
})
->editColumn('code', function (Coupon $coupon) {
return "<code>{$coupon->code}</code>";
})
->orderColumn('status', 'derived_status $1')
->rawColumns(['actions', 'code', 'status'])
->make();
}
}

View file

@ -15,7 +15,7 @@ class InvoiceController extends Controller
$zip = new ZipArchive;
$zip_safe_path = storage_path('invoices.zip');
$res = $zip->open($zip_safe_path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
$result = $this::rglob(storage_path('app/invoice/*'));
$result = $this->rglob(storage_path('app/invoice/*'));
if ($res === true) {
$zip->addFromString('1. Info.txt', __('Created at').' '.now()->format('d.m.Y'));
foreach ($result as $file) {
@ -38,7 +38,7 @@ class InvoiceController extends Controller
{
$files = glob($pattern, $flags);
foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) {
$files = array_merge($files, $this::rglob($dir.'/'.basename($pattern), $flags));
$files = array_merge($files, $this->rglob($dir.'/'.basename($pattern), $flags));
}
return $files;

View file

@ -10,6 +10,8 @@ use Qirolab\Theme\Theme;
class LegalController extends Controller
{
const READ_PERMISSION = "admin.legal.read";
const WRITE_PERMISSION = "admin.legal.write";
/**
* Display
*
@ -17,6 +19,8 @@ class LegalController extends Controller
*/
public function index()
{
$this->checkPermission(self::READ_PERMISSION);
$tos = File::get(Theme::path($path = 'views', "default") . '/information/tos-content.blade.php');
$privacy = File::get(Theme::path($path = 'views', "default") . '/information/privacy-content.blade.php');
$imprint = File::get(Theme::path($path = 'views', "default") . '/information/imprint-content.blade.php');
@ -29,6 +33,8 @@ class LegalController extends Controller
}
public function update(Request $request){
$this->checkPermission(self::READ_PERMISSION);
$tos = $request->tos;
$privacy = $request->privacy;
$imprint = $request->imprint;

View file

@ -2,12 +2,14 @@
namespace App\Http\Controllers\Admin;
use App\Classes\Pterodactyl;
use App\Classes\PterodactylClient;
use App\Settings\PterodactylSettings;
use App\Settings\GeneralSettings;
use App\Http\Controllers\Controller;
use App\Models\Egg;
use App\Models\Location;
use App\Models\Nest;
use App\Models\Node;
use App\Models\Pterodactyl\Egg;
use App\Models\Pterodactyl\Location;
use App\Models\Pterodactyl\Nest;
use App\Models\Pterodactyl\Node;
use App\Models\Payment;
use App\Models\Product;
use App\Models\Server;
@ -17,15 +19,28 @@ use Carbon\Carbon;
class OverViewController extends Controller
{
const READ_PERMISSION = "admin.overview.read";
const SYNC_PERMISSION = "admin.overview.sync";
public const TTL = 86400;
public function index()
private $pterodactyl;
public function __construct(PterodactylSettings $ptero_settings)
{
$this->pterodactyl = new PterodactylClient($ptero_settings);
}
public function index(GeneralSettings $general_settings)
{
$this->checkPermission(self::READ_PERMISSION);
//Get counters
$counters = collect();
//Set basic variables in the collection
$counters->put('users', User::query()->count());
$counters->put('credits', number_format(User::query()->where('role', '!=', 'admin')->sum('credits'), 2, '.', ''));
$counters->put('users', collect());
$counters['users']->active = User::where("suspended", 0)->count();
$counters['users']->total = User::query()->count();
$counters->put('credits', number_format(User::query()->whereHas("roles", function($q){ $q->where("id", "!=", "1"); })->sum('credits'), 2, '.', ''));
$counters->put('payments', Payment::query()->count());
$counters->put('eggs', Egg::query()->count());
$counters->put('nests', Nest::query()->count());
@ -134,7 +149,7 @@ class OverViewController extends Controller
//Get node information and prepare collection
$pteroNodeIds = [];
foreach (Pterodactyl::getNodes() as $pteroNode) {
foreach ($this->pterodactyl->getNodes() as $pteroNode) {
array_push($pteroNodeIds, $pteroNode['attributes']['id']);
}
$nodes = collect();
@ -145,7 +160,7 @@ class OverViewController extends Controller
} //Check if node exists on pterodactyl too, if not, skip
$nodes->put($nodeId, collect());
$nodes[$nodeId]->name = $DBnode['name'];
$pteroNode = Pterodactyl::getNode($nodeId);
$pteroNode = $this->pterodactyl->getNode($nodeId);
$nodes[$nodeId]->usagePercent = round(max($pteroNode['allocated_resources']['memory'] / ($pteroNode['memory'] * ($pteroNode['memory_overallocate'] + 100) / 100), $pteroNode['allocated_resources']['disk'] / ($pteroNode['disk'] * ($pteroNode['disk_overallocate'] + 100) / 100)) * 100, 2);
$counters['totalUsagePercent'] += $nodes[$nodeId]->usagePercent;
@ -156,11 +171,12 @@ class OverViewController extends Controller
}
$counters['totalUsagePercent'] = ($DBnodes->count()) ? round($counters['totalUsagePercent'] / $DBnodes->count(), 2) : 0;
foreach (Pterodactyl::getServers() as $server) { //gets all servers from Pterodactyl and calculates total of credit usage for each node separately + total
foreach ($this->pterodactyl->getServers() as $server) { //gets all servers from Pterodactyl and calculates total of credit usage for each node separately + total
$nodeId = $server['attributes']['node'];
if ($CPServer = Server::query()->where('pterodactyl_id', $server['attributes']['id'])->first()) {
$price = Product::query()->where('id', $CPServer->product_id)->first()->price;
$product = Product::query()->where('id', $CPServer->product_id)->first();
$price = $product->getMonthlyPrice();
if (! $CPServer->suspended) {
$counters['earnings']->active += $price;
$counters['servers']->active++;
@ -207,6 +223,7 @@ class OverViewController extends Controller
'deletedNodesPresent' => ($DBnodes->count() != count($pteroNodeIds)) ? true : false,
'perPageLimit' => ($counters['servers']->total != Server::query()->count()) ? true : false,
'tickets' => $tickets,
'credits_display_name' => $general_settings->credits_display_name
]);
}
@ -215,6 +232,8 @@ class OverViewController extends Controller
*/
public function syncPterodactyl()
{
$this->checkPermission(self::SYNC_PERMISSION);
Node::syncNodes();
Egg::syncEggs();

View file

@ -5,13 +5,21 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\PartnerDiscount;
use App\Models\User;
use App\Settings\LocaleSettings;
use App\Settings\ReferralSettings;
use Illuminate\Http\Request;
class PartnerController extends Controller
{
public function index()
const READ_PERMISSION = "admin.partners.read";
const WRITE_PERMISSION = "admin.partners.write";
public function index(LocaleSettings $locale_settings)
{
return view('admin.partners.index');
$this->checkPermission(self::READ_PERMISSION);
return view('admin.partners.index', [
'locale_datatables' => $locale_settings->datatables
]);
}
/**
@ -21,6 +29,8 @@ class PartnerController extends Controller
*/
public function create()
{
$this->checkPermission(self::WRITE_PERMISSION);
return view('admin.partners.create', [
'partners' => PartnerDiscount::get(),
'users' => User::orderBy('name')->get(),
@ -58,6 +68,8 @@ class PartnerController extends Controller
*/
public function edit(PartnerDiscount $partner)
{
$this->checkPermission(self::WRITE_PERMISSION);
return view('admin.partners.edit', [
'partners' => PartnerDiscount::get(),
'partner' => $partner,
@ -94,6 +106,8 @@ class PartnerController extends Controller
*/
public function destroy(PartnerDiscount $partner)
{
$this->checkPermission(self::WRITE_PERMISSION);
$partner->delete();
return redirect()->back()->with('success', __('partner has been removed!'));
@ -108,29 +122,30 @@ class PartnerController extends Controller
return datatables($query)
->addColumn('actions', function (PartnerDiscount $partner) {
return '
<a data-content="'.__('Edit').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('admin.partners.edit', $partner->id).'" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
<a data-content="'.__('Edit').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('admin.partners.edit', $partner->id).'" class="mr-1 btn btn-sm btn-info"><i class="fas fa-pen"></i></a>
<form class="d-inline" onsubmit="return submitResult();" method="post" action="'.route('admin.partners.destroy', $partner->id).'">
'.csrf_field().'
'.method_field('DELETE').'
<button data-content="'.__('Delete').'" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
<button data-content="'.__('Delete').'" data-toggle="popover" data-trigger="hover" data-placement="top" class="mr-1 btn btn-sm btn-danger"><i class="fas fa-trash"></i></button>
</form>
';
})
->addColumn('user', function (PartnerDiscount $partner) {
return ($user = User::where('id', $partner->user_id)->first()) ? '<a href="'.route('admin.users.show', $partner->user_id).'">'.$user->name.'</a>' : __('Unknown user');
return ($user = User::where('id', $partner->user_id)->first()) ? '<a href="'.route('admin.users.show', $partner->user_id) . '">' . $user->name . '</a>' : __('Unknown user');
})
->editColumn('created_at', function (PartnerDiscount $partner) {
return $partner->created_at ? $partner->created_at->diffForHumans() : '';
})
->editColumn('partner_discount', function (PartnerDiscount $partner) {
return $partner->partner_discount ? $partner->partner_discount.'%' : '0%';
return $partner->partner_discount ? $partner->partner_discount . '%' : '0%';
})
->editColumn('registered_user_discount', function (PartnerDiscount $partner) {
return $partner->registered_user_discount ? $partner->registered_user_discount.'%' : '0%';
return $partner->registered_user_discount ? $partner->registered_user_discount . '%' : '0%';
})
->editColumn('referral_system_commission', function (PartnerDiscount $partner) {
return $partner->referral_system_commission >= 0 ? $partner->referral_system_commission.'%' : __('Default').' ('.config('SETTINGS::REFERRAL:PERCENTAGE').'%)';
->editColumn('referral_system_commission', function (PartnerDiscount $partner, ReferralSettings $referral_settings) {
return $partner->referral_system_commission >= 0 ? $partner->referral_system_commission . '%' : __('Default') . ' ('.$referral_settings->percentage . '%)';
})
->orderColumn('user', 'user_id $1')
->rawColumns(['user', 'actions'])
->make();
}

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Admin;
use App\Events\CouponUsedEvent;
use App\Events\PaymentEvent;
use App\Events\UserUpdateCreditsEvent;
use App\Http\Controllers\Controller;
@ -9,6 +10,7 @@ use App\Models\PartnerDiscount;
use App\Models\Payment;
use App\Models\User;
use App\Models\ShopProduct;
use App\Traits\Coupon as CouponTrait;
use Exception;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
@ -18,17 +20,29 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Helpers\ExtensionHelper;
use App\Settings\CouponSettings;
use App\Settings\GeneralSettings;
use App\Settings\LocaleSettings;
use Illuminate\Support\Facades\Log;
class PaymentController extends Controller
{
const BUY_PERMISSION = 'user.shop.buy';
const VIEW_PERMISSION = "admin.payments.read";
use CouponTrait;
/**
* @return Application|Factory|View
*/
public function index()
public function index(LocaleSettings $locale_settings)
{
$this->checkPermission(self::VIEW_PERMISSION);
return view('admin.payments.index')->with([
'payments' => Payment::paginate(15),
'locale_datatables' => $locale_settings->datatables
]);
}
@ -37,8 +51,10 @@ class PaymentController extends Controller
* @param ShopProduct $shopProduct
* @return Application|Factory|View
*/
public function checkOut(ShopProduct $shopProduct)
public function checkOut(ShopProduct $shopProduct, GeneralSettings $general_settings, CouponSettings $coupon_settings)
{
$this->checkPermission(self::BUY_PERMISSION);
$discount = PartnerDiscount::getDiscount();
$price = $shopProduct->price - ($shopProduct->price * $discount / 100);
@ -49,7 +65,10 @@ class PaymentController extends Controller
// build a paymentgateways array that contains the routes for the payment gateways and the image path for the payment gateway which lays in public/images/Extensions/PaymentGateways with the extensionname in lowercase
foreach ($extensions as $extension) {
$extensionName = basename($extension);
if (!ExtensionHelper::getExtensionConfig($extensionName, 'enabled')) continue; // skip if not enabled
$extensionSettings = ExtensionHelper::getExtensionSettings($extensionName);
if ($extensionSettings->enabled == false) continue;
$payment = new \stdClass();
$payment->name = ExtensionHelper::getExtensionConfig($extensionName, 'name');
@ -58,11 +77,6 @@ class PaymentController extends Controller
}
}
return view('store.checkout')->with([
'product' => $shopProduct,
'discountpercent' => $discount,
@ -73,6 +87,8 @@ class PaymentController extends Controller
'total' => $shopProduct->getTotalPrice(),
'paymentGateways' => $paymentGateways,
'productIsFree' => $price <= 0,
'credits_display_name' => $general_settings->credits_display_name,
'isCouponsEnabled' => $coupon_settings->enabled,
]);
}
@ -113,16 +129,67 @@ class PaymentController extends Controller
public function pay(Request $request)
{
$product = ShopProduct::find($request->product_id);
$paymentGateway = $request->payment_method;
try {
$user = Auth::user();
$user = User::findOrFail($user->id);
$productId = $request->product_id;
$shopProduct = ShopProduct::findOrFail($productId);
$discount = PartnerDiscount::getDiscount();
// on free products, we don't need to use a payment gateway
$realPrice = $product->price - ($product->price * PartnerDiscount::getDiscount() / 100);
if ($realPrice <= 0) {
return $this->handleFreeProduct($product);
$paymentGateway = $request->payment_method;
$couponCode = $request->coupon_code;
$subtotal = $shopProduct->price;
// Apply Coupon
if ($couponCode) {
if ($this->isCouponValid($couponCode, $user, $shopProduct->id)) {
$subtotal = $this->applyCoupon($couponCode, $subtotal);
}
}
// Apply Partner Discount
$subtotal = $subtotal - ($subtotal * $discount / 100);
if ($subtotal <= 0) {
if ($couponCode) {
event(new CouponUsedEvent($couponCode));
}
return $this->handleFreeProduct($shopProduct);
}
// Format the total price to a readable string
$totalPriceString = number_format($subtotal, 2, '.', '');
// create a new payment
$payment = Payment::create([
'user_id' => $user->id,
'payment_id' => null,
'payment_method' => $paymentGateway,
'type' => $shopProduct->type,
'status' => 'open',
'amount' => $shopProduct->quantity,
'price' => $totalPriceString,
'tax_value' => $shopProduct->getTaxValue(),
'tax_percent' => $shopProduct->getTaxPercent(),
'total_price' => $shopProduct->getTotalPrice(),
'currency_code' => $shopProduct->currency_code,
'shop_item_product_id' => $shopProduct->id,
]);
$paymentGatewayExtension = ExtensionHelper::getExtensionClass($paymentGateway);
$redirectUrl = $paymentGatewayExtension::getRedirectUrl($payment, $shopProduct, $totalPriceString);
if ($couponCode) {
event(new CouponUsedEvent($couponCode));
}
} catch (Exception $e) {
Log::error($e->getMessage());
return redirect()->route('store.index')->with('error', __('Oops, something went wrong! Please try again later.'));
}
return redirect()->route('payment.' . $paymentGateway . 'Pay', ['shopProduct' => $product->id]);
return redirect()->away($redirectUrl);
}
/**

View file

@ -3,9 +3,12 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Location;
use App\Models\Nest;
use App\Models\Pterodactyl\Location;
use App\Models\Pterodactyl\Nest;
use App\Models\Product;
use App\Settings\GeneralSettings;
use App\Settings\LocaleSettings;
use App\Settings\UserSettings;
use Exception;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
@ -16,14 +19,22 @@ use Illuminate\Http\Request;
class ProductController extends Controller
{
const READ_PERMISSION = "admin.products.read";
const WRITE_PERMISSION = "admin.products.write";
const EDIT_PERMISSION = "admin.products.edit";
const DELETE_PERMISSION = "admin.products.delete";
/**
* Display a listing of the resource.
*
* @return Application|Factory|View
*/
public function index()
public function index(LocaleSettings $locale_settings)
{
return view('admin.products.index');
$this->checkPermission(self::READ_PERMISSION);
return view('admin.products.index', [
'locale_datatables' => $locale_settings->datatables
]);
}
/**
@ -31,18 +42,23 @@ class ProductController extends Controller
*
* @return Application|Factory|View
*/
public function create()
public function create(GeneralSettings $general_settings)
{
$this->checkPermission(self::WRITE_PERMISSION);
return view('admin.products.create', [
'locations' => Location::with('nodes')->get(),
'nests' => Nest::with('eggs')->get(),
'credits_display_name' => $general_settings->credits_display_name
]);
}
public function clone(Request $request, Product $product)
public function clone(Product $product, GeneralSettings $general_settings)
{
$this->checkPermission(self::WRITE_PERMISSION);
return view('admin.products.create', [
'product' => $product,
'credits_display_name' => $general_settings->credits_display_name,
'locations' => Location::with('nodes')->get(),
'nests' => Nest::with('eggs')->get(),
]);
@ -72,10 +88,14 @@ class ProductController extends Controller
'nodes.*' => 'required|exists:nodes,id',
'eggs.*' => 'required|exists:eggs,id',
'disabled' => 'nullable',
'oom_killer' => 'nullable',
'billing_period' => 'required|in:hourly,daily,weekly,monthly,quarterly,half-annually,annually',
]);
$disabled = ! is_null($request->input('disabled'));
$product = Product::create(array_merge($request->all(), ['disabled' => $disabled]));
$oomkiller = ! is_null($request->input('oom_killer'));
$product = Product::create(array_merge($request->all(), ['disabled' => $disabled, 'oom_killer' => $oomkiller]));
//link nodes and eggs
$product->eggs()->attach($request->input('eggs'));
@ -90,11 +110,14 @@ class ProductController extends Controller
* @param Product $product
* @return Application|Factory|View
*/
public function show(Product $product)
public function show(Product $product, UserSettings $user_settings, GeneralSettings $general_settings)
{
$this->checkPermission(self::READ_PERMISSION);
return view('admin.products.show', [
'product' => $product,
'minimum_credits' => config('SETTINGS::USER:MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER'),
'minimum_credits' => $user_settings->min_credits_to_make_server,
'credits_display_name' => $general_settings->credits_display_name
]);
}
@ -104,12 +127,15 @@ class ProductController extends Controller
* @param Product $product
* @return Application|Factory|View
*/
public function edit(Product $product)
public function edit(Product $product, GeneralSettings $general_settings)
{
$this->checkPermission(self::EDIT_PERMISSION);
return view('admin.products.edit', [
'product' => $product,
'locations' => Location::with('nodes')->get(),
'nests' => Nest::with('eggs')->get(),
'credits_display_name' => $general_settings->credits_display_name
]);
}
@ -138,10 +164,13 @@ class ProductController extends Controller
'nodes.*' => 'required|exists:nodes,id',
'eggs.*' => 'required|exists:eggs,id',
'disabled' => 'nullable',
'oom_killer' => 'nullable',
'billing_period' => 'required|in:hourly,daily,weekly,monthly,quarterly,half-annually,annually',
]);
$disabled = ! is_null($request->input('disabled'));
$product->update(array_merge($request->all(), ['disabled' => $disabled]));
$oomkiller = ! is_null($request->input('oom_killer'));
$product->update(array_merge($request->all(), ['disabled' => $disabled, 'oom_killer' => $oomkiller]));
//link nodes and eggs
$product->eggs()->detach();
@ -157,8 +186,10 @@ class ProductController extends Controller
* @param Product $product
* @return RedirectResponse
*/
public function disable(Request $request, Product $product)
public function disable(Product $product)
{
$this->checkPermission(self::WRITE_PERMISSION);
$product->update(['disabled' => ! $product->disabled]);
return redirect()->route('admin.products.index')->with('success', 'Product has been updated!');
@ -172,6 +203,8 @@ class ProductController extends Controller
*/
public function destroy(Product $product)
{
$this->checkPermission(self::DELETE_PERMISSION);
$servers = $product->servers()->count();
if ($servers > 0) {
return redirect()->back()->with('error', "Product cannot be removed while it's linked to {$servers} servers");
@ -194,14 +227,14 @@ class ProductController extends Controller
return datatables($query)
->addColumn('actions', function (Product $product) {
return '
<a data-content="'.__('Show').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('admin.products.show', $product->id).'" class="btn btn-sm text-white btn-warning mr-1"><i class="fas fa-eye"></i></a>
<a data-content="'.__('Clone').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('admin.products.clone', $product->id).'" class="btn btn-sm text-white btn-primary mr-1"><i class="fas fa-clone"></i></a>
<a data-content="'.__('Edit').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('admin.products.edit', $product->id).'" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
<a data-content="'.__('Show').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('admin.products.show', $product->id).'" class="mr-1 text-white btn btn-sm btn-warning"><i class="fas fa-eye"></i></a>
<a data-content="'.__('Clone').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('admin.products.clone', $product->id).'" class="mr-1 text-white btn btn-sm btn-primary"><i class="fas fa-clone"></i></a>
<a data-content="'.__('Edit').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('admin.products.edit', $product->id).'" class="mr-1 btn btn-sm btn-info"><i class="fas fa-pen"></i></a>
<form class="d-inline" onsubmit="return submitResult();" method="post" action="'.route('admin.products.destroy', $product->id).'">
'.csrf_field().'
'.method_field('DELETE').'
<button data-content="'.__('Delete').'" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
<button data-content="'.__('Delete').'" data-toggle="popover" data-trigger="hover" data-placement="top" class="mr-1 btn btn-sm btn-danger"><i class="fas fa-trash"></i></button>
</form>
';
})
@ -215,22 +248,25 @@ class ProductController extends Controller
->addColumn('eggs', function (Product $product) {
return $product->eggs()->count();
})
->addColumn('disabled', function (Product $product) {
->editColumn('disabled', function (Product $product) {
$checked = $product->disabled == false ? 'checked' : '';
return '
<form class="d-inline" onsubmit="return submitResult();" method="post" action="'.route('admin.products.disable', $product->id).'">
'.csrf_field().'
'.method_field('PATCH').'
<div class="custom-control custom-switch">
<input '.$checked.' name="disabled" onchange="this.form.submit()" type="checkbox" class="custom-control-input" id="switch'.$product->id.'">
<label class="custom-control-label" for="switch'.$product->id.'"></label>
</div>
</form>
<form class="d-inline" onsubmit="return submitResult();" method="post" action="'.route('admin.products.disable', $product->id).'">
'.csrf_field().'
'.method_field('PATCH').'
<div class="custom-control custom-switch">
<input '.$checked.' name="disabled" onchange="this.form.submit()" type="checkbox" class="custom-control-input" id="switch'.$product->id.'">
<label class="custom-control-label" for="switch'.$product->id.'"></label>
</div>
</form>
';
})
->editColumn('minimum_credits', function (Product $product) {
return $product->minimum_credits==-1 ? config('SETTINGS::USER:MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER') : $product->minimum_credits;
->editColumn('minimum_credits', function (Product $product, UserSettings $user_settings) {
return $product->minimum_credits==-1 ? $user_settings->min_credits_to_make_server : $product->minimum_credits;
})
->editColumn('oom_killer', function (Product $product) {
return $product->oom_killer ? __("enabled") : __("disabled");
})
->editColumn('created_at', function (Product $product) {
return $product->created_at ? $product->created_at->diffForHumans() : '';

View file

@ -0,0 +1,219 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use Exception;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class RoleController extends Controller
{
const READ_PERMISSION = "admin.roles.read";
const CREATE_PERMISSION = "admin.roles.create";
const EDIT_PERMISSION = "admin.roles.edit";
const DELETE_PERMISSION = "admin.roles.delete";
/**
* Display a listing of the resource.
*
* @param Request $request
* @return mixed
* @throws Exception
*/
public function index(Request $request)
{
$this->checkPermission(self::READ_PERMISSION);
//datatables
if ($request->ajax()) {
return $this->dataTableQuery();
}
$html = $this->dataTable();
return view('admin.roles.index', compact('html'));
}
/**
* Show the form for creating a new resource.
*
* @return Application|Factory|View
*/
public function create()
{
$this->checkPermission(self::CREATE_PERMISSION);
$permissions = Permission::all();
return view('admin.roles.edit', compact('permissions'));
}
/**
* Store a newly created resource in storage.
*
* @return RedirectResponse
*/
public function store(Request $request): RedirectResponse
{
$this->checkPermission(self::CREATE_PERMISSION);
$role = Role::create([
'name' => $request->name,
'color' => $request->color,
'power' => $request->power
]);
if ($request->permissions) {
$role->givePermissionTo($request->permissions);
}
return redirect()
->route('admin.roles.index')
->with('success', __('Role saved'));
}
/**
* Display the specified resource.
*/
public function show()
{
abort(404);
}
/**
* Show the form for editing the specified resource.
*
* @param Role $role
* @return Application|Factory|View
*/
public function edit(Role $role)
{
$this->checkPermission(self::EDIT_PERMISSION);
if(Auth::user()->roles[0]->power < $role->power){
return back()->with("error","You dont have enough Power to edit that Role");
}
$permissions = Permission::all();
return view('admin.roles.edit', compact('role', 'permissions'));
}
/**
* Update the specified resource in storage.
*
* @param Role $role
* @return RedirectResponse
*/
public function update(Request $request, Role $role)
{
$this->checkPermission(self::EDIT_PERMISSION);
if(Auth::user()->roles[0]->power < $role->power){
return back()->with("error","You dont have enough Power to edit that Role");
}
if ($request->permissions) {
if($role->id != 1){ //disable admin permissions change
$role->syncPermissions($request->permissions);
}
}
//if($role->id == 1 || $role->id == 3 || $role->id == 4){ //dont let the user change the names of these roles
// $role->update([
// 'color' => $request->color
// ]);
//}else{
$role->update([
'name' => $request->name,
'color' => $request->color
]);
//}
//if($role->id == 1){
// return redirect()->route('admin.roles.index')->with('success', __('Role updated. Name and Permissions of this Role cannot be changed'));
//}elseif($role->id == 4 || $role->id == 3){
// return redirect()->route('admin.roles.index')->with('success', __('Role updated. Name of this Role cannot be changed'));
// }else{
return redirect()
->route('admin.roles.index')
->with('success', __('Role saved'));
//}
}
/**
* Remove the specified resource from storage.
*
* @return RedirectResponse
*/
public function destroy(Role $role)
{
$this->checkPermission(self::DELETE_PERMISSION);
if($role->id == 1 || $role->id == 3 || $role->id == 4){ //cannot delete the hard coded roles
return back()->with("error","You cannot delete that role");
}
$users = User::role($role)->get();
foreach($users as $user){
//$user->syncRoles(['Member']);
$user->syncRoles(4);
}
$role->delete();
return redirect()
->route('admin.roles.index')
->with('success', __('Role removed'));
}
/**
* @return mixed
* @throws Exception
*/
public function dataTable()
{
$query = Role::query()->withCount(['users', 'permissions'])->get();
return datatables($query)
->editColumn('id', function (Role $role) {
return $role->id;
})
->addColumn('actions', function (Role $role) {
return '
<a title="Edit" href="'.route("admin.roles.edit", $role).'" class="btn btn-sm btn-info"><i
class="fa fas fa-edit"></i></a>
<form class="d-inline" method="post" action="'.route("admin.roles.destroy", $role).'">
' . csrf_field() . '
' . method_field("DELETE") . '
<button title="Delete" type="submit" class="btn btn-sm btn-danger confirm"><i
class="fa fas fa-trash"></i></button>
</form>
';
})
->editColumn('name', function (Role $role) {
return "<span style='background-color: $role->color' class='badge'>$role->name</span>";
})
->editColumn('users_count', function ($query) {
return $query->users_count;
})
->editColumn('permissions_count', function ($query){
return $query->permissions_count;
})
->editColumn('power', function (Role $role){
return $role->power;
})
->rawColumns(['actions', 'name'])
->make(true);
}
}

View file

@ -2,10 +2,12 @@
namespace App\Http\Controllers\Admin;
use App\Classes\Pterodactyl;
use App\Http\Controllers\Controller;
use App\Models\Server;
use App\Models\User;
use App\Settings\LocaleSettings;
use App\Settings\PterodactylSettings;
use App\Classes\PterodactylClient;
use Exception;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
@ -18,14 +20,32 @@ use Illuminate\Support\Facades\Log;
class ServerController extends Controller
{
const READ_PERMISSION = "admin.servers.read";
const WRITE_PERMISSION = "admin.servers.write";
const SUSPEND_PERMISSION = "admin.servers.suspend";
const CHANGEOWNER_PERMISSION = "admin.servers.write.owner";
const CHANGE_IDENTIFIER_PERMISSION = "admin.servers.write.identifier";
const DELETE_PERMISSION = "admin.servers.delete";
private $pterodactyl;
public function __construct(PterodactylSettings $ptero_settings)
{
$this->pterodactyl = new PterodactylClient($ptero_settings);
}
/**
* Display a listing of the resource.
*
* @return Application|Factory|View|Response
*/
public function index()
public function index(LocaleSettings $locale_settings)
{
return view('admin.servers.index');
$this->checkPermission(self::READ_PERMISSION);
return view('admin.servers.index', [
'locale_datatables' => $locale_settings->datatables
]);
}
/**
@ -36,6 +56,8 @@ class ServerController extends Controller
*/
public function edit(Server $server)
{
$this->checkPermission(self::WRITE_PERMISSION);
// get all users from the database
$users = User::all();
@ -59,13 +81,13 @@ class ServerController extends Controller
]);
if ($request->get('user_id') != $server->user_id) {
if ($request->get('user_id') != $server->user_id && $this->can(self::CHANGEOWNER_PERMISSION)) {
// find the user
$user = User::findOrFail($request->get('user_id'));
// try to update the owner on pterodactyl
try {
$response = Pterodactyl::updateServerOwner($server, $user->pterodactyl_id);
$response = $this->pterodactyl->updateServerOwner($server, $user->pterodactyl_id);
if ($response->getStatusCode() != 200) {
return redirect()->back()->with('error', 'Failed to update server owner on pterodactyl');
}
@ -78,7 +100,10 @@ class ServerController extends Controller
}
// update the identifier
$server->identifier = $request->get('identifier');
if ($this->can(self::CHANGE_IDENTIFIER_PERMISSION)) {
$server->identifier = $request->get('identifier');
}
$server->save();
return redirect()->route('admin.servers.index')->with('success', 'Server updated!');
@ -92,6 +117,7 @@ class ServerController extends Controller
*/
public function destroy(Server $server)
{
$this->checkPermission(self::DELETE_PERMISSION);
try {
$server->delete();
@ -102,11 +128,31 @@ class ServerController extends Controller
}
/**
* @param Server $server
* Cancel the Server billing cycle.
*
* @param Server $server
* @return RedirectResponse|Response
*/
public function cancel(Server $server)
{
try {
$server->update([
'canceled' => now(),
]);
return redirect()->route('servers.index')->with('success', __('Server canceled'));
} catch (Exception $e) {
return redirect()->route('servers.index')->with('error', __('An exception has occurred while trying to cancel the server"') . $e->getMessage() . '"');
}
}
/**
* @param Server $server
* @return RedirectResponse
*/
public function toggleSuspended(Server $server)
{
$this->checkPermission(self::SUSPEND_PERMISSION);
try {
$server->isSuspended() ? $server->unSuspend() : $server->suspend();
} catch (Exception $exception) {
@ -118,7 +164,6 @@ class ServerController extends Controller
public function syncServers()
{
$pteroServers = Pterodactyl::getServers();
$CPServers = Server::get();
$CPIDArray = [];
@ -129,7 +174,7 @@ class ServerController extends Controller
}
}
foreach ($pteroServers as $server) { //go thru all ptero servers, if server exists, change value to true in array.
foreach ($this->pterodactyl->getServers() as $server) { //go thru all ptero servers, if server exists, change value to true in array.
if (isset($CPIDArray[$server['attributes']['id']])) {
$CPIDArray[$server['attributes']['id']] = true;
@ -149,7 +194,7 @@ class ServerController extends Controller
}, ARRAY_FILTER_USE_BOTH); //Array of servers, that dont exist on ptero (value == false)
$deleteCount = 0;
foreach ($filteredArray as $key => $CPID) { //delete servers that dont exist on ptero anymore
if (!Pterodactyl::getServerAttributes($key, true)) {
if (!$this->pterodactyl->getServerAttributes($key, true)) {
$deleteCount++;
}
}
@ -183,7 +228,7 @@ class ServerController extends Controller
return '<a href="' . route('admin.users.show', $server->user->id) . '">' . $server->user->name . '</a>';
})
->addColumn('resources', function (Server $server) {
return $server->product->description;
return $server->product->name;
})
->addColumn('actions', function (Server $server) {
$suspendColor = $server->isSuspended() ? 'btn-success' : 'btn-warning';
@ -216,8 +261,8 @@ class ServerController extends Controller
->editColumn('suspended', function (Server $server) {
return $server->suspended ? $server->suspended->diffForHumans() : '';
})
->editColumn('name', function (Server $server) {
return '<a class="text-info" target="_blank" href="' . config('SETTINGS::SYSTEM:PTERODACTYL:URL') . '/admin/servers/view/' . $server->pterodactyl_id . '">' . strip_tags($server->name) . '</a>';
->editColumn('name', function (Server $server, PterodactylSettings $ptero_settings) {
return '<a class="text-info" target="_blank" href="' . $ptero_settings->panel_url . '/admin/servers/view/' . $server->pterodactyl_id . '">' . strip_tags($server->name) . '</a>';
})
->rawColumns(['user', 'actions', 'status', 'name'])
->make();

View file

@ -2,15 +2,21 @@
namespace App\Http\Controllers\Admin;
use App\Helpers\ExtensionHelper;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Response;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Validator;
use Qirolab\Theme\Theme;
class SettingsController extends Controller
{
/**
* Display a listing of the resource.
*
@ -20,36 +26,138 @@ class SettingsController extends Controller
{
//Get all tabs as laravel view paths
$tabs = [];
if(file_exists(Theme::getViewPaths()[0] . '/admin/settings/tabs/')){
$tabspath = glob(Theme::getViewPaths()[0] . '/admin/settings/tabs/*.blade.php');
}else{
$tabspath = glob(Theme::path($path = 'views', $themeName = 'default').'/admin/settings/tabs/*.blade.php');
}
// get all other settings in app/Settings directory
// group items by file name like $categories
$settings = collect();
$settings_classes = [];
foreach ($tabspath as $filename) {
$tabs[] = 'admin.settings.tabs.'.basename($filename, '.blade.php');
// get all app settings
$app_settings = scandir(app_path('Settings'));
$app_settings = array_diff($app_settings, ['.', '..']);
// append App\Settings to class name
foreach ($app_settings as $app_setting) {
$settings_classes[] = 'App\\Settings\\' . str_replace('.php', '', $app_setting);
}
// get all extension settings
$settings_files = array_merge($settings_classes, ExtensionHelper::getAllExtensionSettingsClasses());
//Generate a html list item for each tab based on tabs file basename, set first tab as active
$tabListItems = [];
foreach ($tabs as $tab) {
$tabName = str_replace('admin.settings.tabs.', '', $tab);
$tabListItems[] = '<li class="nav-item">
<a class="nav-link '.(empty($tabListItems) ? 'active' : '').'" data-toggle="pill" href="#'.$tabName.'">
'.__(ucfirst($tabName)).'
</a></li>';
foreach ($settings_files as $file) {
$className = $file;
// instantiate the class and call toArray method to get all options
$options = (new $className())->toArray();
// call getOptionInputData method to get all options
if (method_exists($className, 'getOptionInputData')) {
$optionInputData = $className::getOptionInputData();
} else {
$optionInputData = [];
}
// collect all option input data
$optionsData = [];
foreach ($options as $key => $value) {
$optionsData[$key] = [
'value' => $value,
'label' => $optionInputData[$key]['label'] ?? ucwords(str_replace('_', ' ', $key)),
'type' => $optionInputData[$key]['type'] ?? 'string',
'description' => $optionInputData[$key]['description'] ?? '',
'options' => $optionInputData[$key]['options'] ?? [],
'identifier' => $optionInputData[$key]['identifier'] ?? 'option'
];
}
// collect category icon if available
if (isset($optionInputData['category_icon'])) {
$optionsData['category_icon'] = $optionInputData['category_icon'];
}
$optionsData['settings_class'] = $className;
$settings[str_replace('Settings', '', class_basename($className))] = $optionsData;
}
$settings->sort();
$themes = array_diff(scandir(base_path('themes')), array('..', '.'));
return view('admin.settings.index', [
'tabs' => $tabs,
'tabListItems' => $tabListItems,
'settings' => $settings->all(),
'themes' => $themes,
'active_theme' => Theme::active(),
]);
}
/**
* Update the specified resource in storage.
*
*/
public function update(Request $request)
{
$category = request()->get('category');
$this->checkPermission("settings." . strtolower($category) . ".write");
$settings_class = request()->get('settings_class');
if (method_exists($settings_class, 'getValidations')) {
$validations = $settings_class::getValidations();
} else {
$validations = [];
}
$validator = Validator::make($request->all(), $validations);
if ($validator->fails()) {
return Redirect::to('admin/settings' . '#' . $category)->withErrors($validator)->withInput();
}
$settingsClass = new $settings_class();
foreach ($settingsClass->toArray() as $key => $value) {
// Get the type of the settingsclass property
$rp = new \ReflectionProperty($settingsClass, $key);
$rpType = $rp->getType();
if ($rpType == 'bool') {
$settingsClass->$key = $request->has($key);
continue;
}
if ($rp->name == 'available') {
$settingsClass->$key = implode(",", $request->$key);
continue;
}
$nullable = $rpType->allowsNull();
if ($nullable) $settingsClass->$key = $request->input($key) ?? null;
else $settingsClass->$key = $request->input($key);
}
$settingsClass->save();
return Redirect::to('admin/settings' . '#' . $category)->with('success', 'Settings updated successfully.');
}
public function updateIcons(Request $request)
{
$request->validate([
'icon' => 'nullable|max:10000|mimes:jpg,png,jpeg',
'logo' => 'nullable|max:10000|mimes:jpg,png,jpeg',
'favicon' => 'nullable|max:10000|mimes:ico',
]);
if ($request->hasFile('icon')) {
$request->file('icon')->storeAs('public', 'icon.png');
}
if ($request->hasFile('logo')) {
$request->file('logo')->storeAs('public', 'logo.png');
}
if ($request->hasFile('favicon')) {
$request->file('favicon')->storeAs('public', 'favicon.ico');
}
return Redirect::to('admin/settings')->with('success', 'Icons updated successfully.');
}
}

View file

@ -2,38 +2,40 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ShopProduct;
use App\Settings\GeneralSettings;
use App\Settings\LocaleSettings;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller;
use Illuminate\Validation\Rule;
class ShopProductController extends Controller
{
const READ_PERMISSION = 'admin.store.read';
const WRITE_PERMISSION = 'admin.store.write';
const DISABLE_PERMISSION = 'admin.store.disable';
/**
* Display a listing of the resource.
*
* @return Application|Factory|View|Response
*/
public function index(Request $request)
public function index(LocaleSettings $locale_settings, GeneralSettings $general_settings)
{
$isPaymentSetup = false;
$this->checkPermission(self::READ_PERMISSION);
$isStoreEnabled = $general_settings->store_enabled;
if (
env('APP_ENV') == 'local' ||
config('SETTINGS::PAYMENTS:PAYPAL:SECRET') && config('SETTINGS::PAYMENTS:PAYPAL:CLIENT_ID') ||
config('SETTINGS::PAYMENTS:STRIPE:SECRET') && config('SETTINGS::PAYMENTS:STRIPE:ENDPOINT_SECRET') && config('SETTINGS::PAYMENTS:STRIPE:METHODS')
) {
$isPaymentSetup = true;
}
return view('admin.store.index', [
'isPaymentSetup' => $isPaymentSetup,
'isStoreEnabled' => $isStoreEnabled,
'locale_datatables' => $locale_settings->datatables
]);
}
@ -42,10 +44,13 @@ class ShopProductController extends Controller
*
* @return Application|Factory|View|Response
*/
public function create()
public function create(GeneralSettings $general_settings)
{
$this->checkPermission(self::WRITE_PERMISSION);
return view('admin.store.create', [
'currencyCodes' => config('currency_codes'),
'credits_display_name' => $general_settings->credits_display_name
]);
}
@ -79,11 +84,14 @@ class ShopProductController extends Controller
* @param ShopProduct $shopProduct
* @return Application|Factory|View|Response
*/
public function edit(ShopProduct $shopProduct)
public function edit(ShopProduct $shopProduct, GeneralSettings $general_settings)
{
$this->checkPermission(self::WRITE_PERMISSION);
return view('admin.store.edit', [
'currencyCodes' => config('currency_codes'),
'shopProduct' => $shopProduct,
'credits_display_name' => $general_settings->credits_display_name
]);
}
@ -117,8 +125,10 @@ class ShopProductController extends Controller
* @param ShopProduct $shopProduct
* @return RedirectResponse
*/
public function disable(Request $request, ShopProduct $shopProduct)
public function disable(ShopProduct $shopProduct)
{
$this->checkPermission(self::DISABLE_PERMISSION);
$shopProduct->update(['disabled' => !$shopProduct->disabled]);
return redirect()->route('admin.store.index')->with('success', __('Product has been updated!'));
@ -132,6 +142,7 @@ class ShopProductController extends Controller
*/
public function destroy(ShopProduct $shopProduct)
{
$this->checkPermission(self::WRITE_PERMISSION);
$shopProduct->delete();
return redirect()->back()->with('success', __('Store item has been removed!'));
@ -145,16 +156,16 @@ class ShopProductController extends Controller
return datatables($query)
->addColumn('actions', function (ShopProduct $shopProduct) {
return '
<a data-content="' . __('Edit') . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.store.edit', $shopProduct->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
<a data-content="' . __('Edit') . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.store.edit', $shopProduct->id) . '" class="mr-1 btn btn-sm btn-info"><i class="fas fa-pen"></i></a>
<form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.store.destroy', $shopProduct->id) . '">
' . csrf_field() . '
' . method_field('DELETE') . '
<button data-content="' . __('Delete') . '" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
<button data-content="' . __('Delete') . '" data-toggle="popover" data-trigger="hover" data-placement="top" class="mr-1 btn btn-sm btn-danger"><i class="fas fa-trash"></i></button>
</form>
';
})
->addColumn('disabled', function (ShopProduct $shopProduct) {
->editColumn('disabled', function (ShopProduct $shopProduct) {
$checked = $shopProduct->disabled == false ? 'checked' : '';
return '

View file

@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\Moderation;
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
@ -9,15 +9,20 @@ use Illuminate\Http\Request;
class TicketCategoryController extends Controller
{
const READ_PERMISSION = "admin.tickets.category.read";
const WRITE_PERMISSION = "admin.tickets.category.write";
/**
*
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$this->checkPermission(self::READ_PERMISSION);
$categories = TicketCategory::all();
return view('moderator.ticket.category')->with("categories",$categories);
return view('admin.ticket.category')->with("categories",$categories);
}
/**
@ -28,6 +33,8 @@ class TicketCategoryController extends Controller
*/
public function store(Request $request)
{
$this->checkPermission(self::WRITE_PERMISSION);
$request->validate([
'name' => 'required|string|max:191',
]);
@ -35,7 +42,7 @@ class TicketCategoryController extends Controller
TicketCategory::create($request->all());
return redirect(route("moderator.ticket.category.index"))->with("success",__("Category created"));
return redirect(route("admin.ticket.category.index"))->with("success",__("Category created"));
}
/**
@ -46,6 +53,8 @@ class TicketCategoryController extends Controller
*/
public function update(Request $request)
{
$this->checkPermission(self::WRITE_PERMISSION);
$request->validate([
'category' => 'required|int',
'name' => 'required|string|max:191',
@ -68,6 +77,8 @@ class TicketCategoryController extends Controller
*/
public function destroy($id)
{
$this->checkPermission(self::WRITE_PERMISSION);
$category = TicketCategory::where("id",$id)->firstOrFail();
if($category->id == 5 ){ //cannot delete "other" category
@ -84,7 +95,7 @@ class TicketCategoryController extends Controller
$category->delete();
return redirect()
->route('moderator.ticket.category.index')
->route('admin.ticket.category.index')
->with('success', __('Category removed'));
}
@ -101,7 +112,7 @@ class TicketCategoryController extends Controller
})
->addColumn('actions', function (TicketCategory $category) {
return '
<form class="d-inline" onsubmit="return submitResult();" method="post" action="'.route('moderator.ticket.category.destroy', $category->id).'">
<form class="d-inline" onsubmit="return submitResult();" method="post" action="'.route('admin.ticket.category.destroy', $category->id).'">
'.csrf_field().'
'.method_field('DELETE').'
<button data-content="'.__('Delete').'" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>

View file

@ -1,8 +1,9 @@
<?php
namespace App\Http\Controllers\Moderation;
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Moderation\Exception;
use App\Models\Server;
use App\Models\Ticket;
use App\Models\TicketBlacklist;
@ -10,21 +11,32 @@ use App\Models\TicketCategory;
use App\Models\TicketComment;
use App\Models\User;
use App\Notifications\Ticket\User\ReplyNotification;
use App\Settings\LocaleSettings;
use App\Settings\PterodactylSettings;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class TicketsController extends Controller
{
public function index()
{
$tickets = Ticket::orderBy('id', 'desc')->paginate(10);
$ticketcategories = TicketCategory::all();
const READ_PERMISSION = "admin.tickets.read";
const WRITE_PERMISSION = "admin.tickets.write";
return view('moderator.ticket.index', compact('tickets', 'ticketcategories'));
const BLACKLIST_READ_PERMISSION ='admin.ticket_blacklist.read';
const BLACKLIST_WRITE_PERMISSION ='admin.ticket_blacklist.write';
public function index(LocaleSettings $locale_settings)
{
$this->checkPermission(self::READ_PERMISSION);
return view('admin.ticket.index', [
'tickets' => Ticket::orderBy('id', 'desc')->paginate(10),
'ticketcategories' => TicketCategory::all(),
'locale_datatables' => $locale_settings->datatables
]);
}
public function show($ticket_id)
public function show($ticket_id, PterodactylSettings $ptero_settings)
{
$this->checkPermission(self::READ_PERMISSION);
try {
$ticket = Ticket::where('ticket_id', $ticket_id)->firstOrFail();
} catch (Exception $e)
@ -34,12 +46,14 @@ class TicketsController extends Controller
$ticketcomments = $ticket->ticketcomments;
$ticketcategory = $ticket->ticketcategory;
$server = Server::where('id', $ticket->server)->first();
$pterodactyl_url = $ptero_settings->panel_url;
return view('moderator.ticket.show', compact('ticket', 'ticketcategory', 'ticketcomments', 'server'));
return view('admin.ticket.show', compact('ticket', 'ticketcategory', 'ticketcomments', 'server', 'pterodactyl_url'));
}
public function changeStatus($ticket_id)
{
$this->checkPermission(self::WRITE_PERMISSION);
try {
$ticket = Ticket::where('ticket_id', $ticket_id)->firstOrFail();
} catch(Exception $e)
@ -61,6 +75,7 @@ class TicketsController extends Controller
public function delete($ticket_id)
{
$this->checkPermission(self::WRITE_PERMISSION);
try {
$ticket = Ticket::where('ticket_id', $ticket_id)->firstOrFail();
} catch (Exception $e)
@ -76,6 +91,9 @@ class TicketsController extends Controller
public function reply(Request $request)
{
$this->checkPermission(self::WRITE_PERMISSION);
$this->validate($request, ['ticketcomment' => 'required']);
try {
$ticket = Ticket::where('id', $request->input('ticket_id'))->firstOrFail();
@ -103,14 +121,15 @@ class TicketsController extends Controller
public function dataTable()
{
$query = Ticket::query();
$query = Ticket::leftJoin('ticket_categories', 'tickets.ticketcategory_id', '=', 'ticket_categories.id')
->select(['tickets.*', 'ticket_categories.name as category_name']);
return datatables($query)
->addColumn('category', function (Ticket $tickets) {
return $tickets->ticketcategory->name;
->addColumn('category', function (Ticket $ticket) {
return $ticket->category_name;
})
->editColumn('title', function (Ticket $tickets) {
return '<a class="text-info" href="'.route('moderator.ticket.show', ['ticket_id' => $tickets->ticket_id]).'">'.'#'.$tickets->ticket_id.' - '.htmlspecialchars($tickets->title).'</a>';
return '<a class="text-info" href="'.route('admin.ticket.show', ['ticket_id' => $tickets->ticket_id]).'">'.'#'.$tickets->ticket_id.' - '.htmlspecialchars($tickets->title).'</a>';
})
->editColumn('user_id', function (Ticket $tickets) {
return '<a href="'.route('admin.users.show', $tickets->user->id).'">'.$tickets->user->name.'</a>';
@ -121,16 +140,16 @@ class TicketsController extends Controller
$statusButtonText = ($tickets->status == "Closed") ? __('Reopen') : __('Close');
return '
<a data-content="'.__('View').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('moderator.ticket.show', ['ticket_id' => $tickets->ticket_id]).'" class="btn btn-sm text-white btn-info mr-1"><i class="fas fa-eye"></i></a>
<form class="d-inline" method="post" action="'.route('moderator.ticket.changeStatus', ['ticket_id' => $tickets->ticket_id]).'">
<a data-content="'.__('View').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('admin.ticket.show', ['ticket_id' => $tickets->ticket_id]).'" class="mr-1 text-white btn btn-sm btn-info"><i class="fas fa-eye"></i></a>
<form class="d-inline" method="post" action="'.route('admin.ticket.changeStatus', ['ticket_id' => $tickets->ticket_id]).'">
'.csrf_field().'
'.method_field('POST').'
<button data-content="'.__($statusButtonText).'" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm text-white '.$statusButtonColor.' mr-1"><i class="fas '.$statusButtonIcon.'"></i></button>
<button data-content="'.__($statusButtonText).'" data-toggle="popover" data-trigger="hover" data-placement="top" class="text-white btn btn-sm '.$statusButtonColor.' mr-1"><i class="fas '.$statusButtonIcon.'"></i></button>
</form>
<form class="d-inline" method="post" action="'.route('moderator.ticket.delete', ['ticket_id' => $tickets->ticket_id]).'">
<form class="d-inline" method="post" action="'.route('admin.ticket.delete', ['ticket_id' => $tickets->ticket_id]).'">
'.csrf_field().'
'.method_field('POST').'
<button data-content="'.__('Delete').'" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm text-white btn-danger mr-1"><i class="fas fa-trash"></i></button>
<button data-content="'.__('Delete').'" data-toggle="popover" data-trigger="hover" data-placement="top" class="mr-1 text-white btn btn-sm btn-danger"><i class="fas fa-trash"></i></button>
</form>
';
})
@ -160,17 +179,24 @@ class TicketsController extends Controller
return ['display' => $tickets->updated_at ? $tickets->updated_at->diffForHumans() : '',
'raw' => $tickets->updated_at ? strtotime($tickets->updated_at) : ''];
})
->rawColumns(['category', 'title', 'user_id', 'status', 'priority', 'updated_at', 'actions'])
->orderColumn('category', 'category_name $1')
->rawColumns(['title', 'user_id', 'status', 'priority', 'updated_at', 'actions'])
->make(true);
}
public function blacklist()
public function blacklist(LocaleSettings $locale_settings)
{
return view('moderator.ticket.blacklist');
$this->checkPermission(self::BLACKLIST_READ_PERMISSION);
return view('admin.ticket.blacklist', [
'locale_datatables' => $locale_settings->datatables
]);
}
public function blacklistAdd(Request $request)
{
$this->checkPermission(self::BLACKLIST_WRITE_PERMISSION);
try {
$user = User::where('id', $request->user_id)->firstOrFail();
$check = TicketBlacklist::where('user_id', $user->id)->first();
@ -196,6 +222,8 @@ class TicketsController extends Controller
public function blacklistDelete($id)
{
$this->checkPermission(self::BLACKLIST_WRITE_PERMISSION);
$blacklist = TicketBlacklist::where('id', $id)->first();
$blacklist->delete();
@ -204,6 +232,8 @@ class TicketsController extends Controller
public function blacklistChange($id)
{
$this->checkPermission(self::BLACKLIST_WRITE_PERMISSION);
try {
$blacklist = TicketBlacklist::where('id', $id)->first();
}
@ -248,15 +278,15 @@ class TicketsController extends Controller
})
->addColumn('actions', function (TicketBlacklist $blacklist) {
return '
<form class="d-inline" method="post" action="'.route('moderator.ticket.blacklist.change', ['id' => $blacklist->id]).'">
<form class="d-inline" method="post" action="'.route('admin.ticket.blacklist.change', ['id' => $blacklist->id]).'">
'.csrf_field().'
'.method_field('POST').'
<button data-content="'.__('Change Status').'" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm text-white btn-warning mr-1"><i class="fas fa-sync-alt"></i></button>
<button data-content="'.__('Change Status').'" data-toggle="popover" data-trigger="hover" data-placement="top" class="mr-1 text-white btn btn-sm btn-warning"><i class="fas fa-sync-alt"></i></button>
</form>
<form class="d-inline" method="post" action="'.route('moderator.ticket.blacklist.delete', ['id' => $blacklist->id]).'">
<form class="d-inline" method="post" action="'.route('admin.ticket.blacklist.delete', ['id' => $blacklist->id]).'">
'.csrf_field().'
'.method_field('POST').'
<button data-content="'.__('Delete').'" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm text-white btn-danger mr-1"><i class="fas fa-trash"></i></button>
<button data-content="'.__('Delete').'" data-toggle="popover" data-trigger="hover" data-placement="top" class="mr-1 text-white btn btn-sm btn-danger"><i class="fas fa-trash"></i></button>
</form>
';
})

View file

@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
use App\Enums\UsefulLinkLocation;
use App\Http\Controllers\Controller;
use App\Models\UsefulLink;
use App\Settings\LocaleSettings;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
@ -14,14 +15,19 @@ use Illuminate\Http\Response;
class UsefulLinkController extends Controller
{
const READ_PERMISSION = "admin.useful_links.read";
const WRITE_PERMISSION = "admin.useful_links.write";
/**
* Display a listing of the resource.
*
* @return Application|Factory|View|Response
*/
public function index()
public function index(LocaleSettings $locale_settings)
{
return view('admin.usefullinks.index');
$this->checkPermission(self::READ_PERMISSION);
return view('admin.usefullinks.index', [
'locale_datatables' => $locale_settings->datatables
]);
}
/**
@ -31,6 +37,7 @@ class UsefulLinkController extends Controller
*/
public function create()
{
$this->checkPermission(self::WRITE_PERMISSION);
$positions = UsefulLinkLocation::cases();
return view('admin.usefullinks.create')->with('positions', $positions);
}
@ -81,6 +88,8 @@ class UsefulLinkController extends Controller
*/
public function edit(UsefulLink $usefullink)
{
$this->checkPermission(self::WRITE_PERMISSION);
$positions = UsefulLinkLocation::cases();
return view('admin.usefullinks.edit', [
'link' => $usefullink,
@ -123,6 +132,7 @@ class UsefulLinkController extends Controller
*/
public function destroy(UsefulLink $usefullink)
{
$this->checkPermission(self::WRITE_PERMISSION);
$usefullink->delete();
return redirect()->back()->with('success', __('product has been removed!'));

View file

@ -2,11 +2,14 @@
namespace App\Http\Controllers\Admin;
use App\Classes\Pterodactyl;
use App\Events\UserUpdateCreditsEvent;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Notifications\DynamicNotification;
use App\Settings\LocaleSettings;
use App\Settings\PterodactylSettings;
use App\Classes\PterodactylClient;
use App\Settings\GeneralSettings;
use Exception;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
@ -23,15 +26,29 @@ use Illuminate\Support\HtmlString;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\Permission\Models\Role;
class UserController extends Controller
{
const READ_PERMISSION = "admin.users.read";
const WRITE_PERMISSION = "admin.users.write";
const SUSPEND_PERMISSION = "admin.users.suspend";
const CHANGE_EMAIL_PERMISSION = "admin.users.write.email";
const CHANGE_CREDITS_PERMISSION = "admin.users.write.credits";
const CHANGE_USERNAME_PERMISSION = "admin.users.write.username";
const CHANGE_PASSWORD_PERMISSION = "admin.users.write.password";
const CHANGE_ROLE_PERMISSION ="admin.users.write.role";
const CHANGE_REFERAL_PERMISSION ="admin.users.write.referal";
const CHANGE_PTERO_PERMISSION = "admin.users.write.pterodactyl";
const DELETE_PERMISSION = "admin.users.delete";
const NOTIFY_PERMISSION = "admin.users.notify";
const LOGIN_PERMISSION = "admin.users.login_as";
private Pterodactyl $pterodactyl;
private $pterodactyl;
public function __construct(Pterodactyl $pterodactyl)
public function __construct(PterodactylSettings $ptero_settings)
{
$this->pterodactyl = $pterodactyl;
$this->pterodactyl = new PterodactylClient($ptero_settings);
}
/**
@ -40,9 +57,14 @@ class UserController extends Controller
* @param Request $request
* @return Application|Factory|View|Response
*/
public function index(Request $request)
public function index(LocaleSettings $locale_settings, GeneralSettings $general_settings)
{
return view('admin.users.index');
$this->checkPermission(self::READ_PERMISSION);
return view('admin.users.index', [
'locale_datatables' => $locale_settings->datatables,
'credits_display_name' => $general_settings->credits_display_name
]);
}
/**
@ -51,8 +73,10 @@ class UserController extends Controller
* @param User $user
* @return Application|Factory|View|Response
*/
public function show(User $user)
public function show(User $user, LocaleSettings $locale_settings, GeneralSettings $general_settings)
{
$this->checkPermission(self::READ_PERMISSION);
//QUERY ALL REFERRALS A USER HAS
//i am not proud of this at all.
$allReferals = [];
@ -65,6 +89,8 @@ class UserController extends Controller
return view('admin.users.show')->with([
'user' => $user,
'referrals' => $allReferals,
'locale_datatables' => $locale_settings->datatables,
'credits_display_name' => $general_settings->credits_display_name
]);
}
@ -99,10 +125,15 @@ class UserController extends Controller
* @param User $user
* @return Application|Factory|View|Response
*/
public function edit(User $user)
public function edit(User $user, GeneralSettings $general_settings)
{
$this->checkPermission(self::WRITE_PERMISSION);
$roles = Role::all();
return view('admin.users.edit')->with([
'user' => $user,
'credits_display_name' => $general_settings->credits_display_name,
'roles' => $roles
]);
}
@ -117,23 +148,27 @@ class UserController extends Controller
*/
public function update(Request $request, User $user)
{
$request->validate([
$data = $request->validate([
'name' => 'required|string|min:4|max:30',
'pterodactyl_id' => "required|numeric|unique:users,pterodactyl_id,{$user->id}",
'email' => 'required|string|email',
'credits' => 'required|numeric|min:0|max:99999999',
'server_limit' => 'required|numeric|min:0|max:1000000',
'role' => Rule::in(['admin', 'moderator', 'client', 'member']),
'referral_code' => "required|string|min:2|max:32|unique:users,referral_code,{$user->id}",
]);
//update roles
if ($request->roles && $this->can(self::CHANGE_ROLE_PERMISSION)) {
$user->syncRoles($request->roles);
}
if (isset($this->pterodactyl->getUser($request->input('pterodactyl_id'))['errors'])) {
throw ValidationException::withMessages([
'pterodactyl_id' => [__("User does not exists on pterodactyl's panel")],
]);
}
if (!is_null($request->input('new_password'))) {
if (!is_null($request->input('new_password')) && $this->can(self::CHANGE_PASSWORD_PERMISSION)) {
$request->validate([
'new_password' => 'required|string|min:8',
'new_password_confirmation' => 'required|same:new_password',
@ -144,7 +179,24 @@ class UserController extends Controller
]);
}
$user->update($request->all());
// if($this->can(self::CHANGE_USERNAME_PERMISSION)){
// $user->name = $request->name;
// }
// if($this->can(self::CHANGE_CREDITS_PERMISSION)){
// $user->credits = $request->credits;
// }
// if($this->can(self::CHANGE_PTERO_PERMISSION)){
// $user->pterodactyl_id = $request->pterodactyl_id;
// }
// if($this->can(self::CHANGE_REFERAL_PERMISSION)){
// $user->referral_code = $request->referral_code;
// }
// if($this->can(self::CHANGE_EMAIL_PERMISSION)){
// $user->email = $request->email;
// }
$user->update($data);
event(new UserUpdateCreditsEvent($user));
return redirect()->route('admin.users.index')->with('success', 'User updated!');
@ -158,6 +210,12 @@ class UserController extends Controller
*/
public function destroy(User $user)
{
$this->checkPermission(self::DELETE_PERMISSION);
if ($user->hasRole(1) && User::role(1)->count() === 1) {
return redirect()->back()->with('error', __('You can not delete the last admin!'));
}
$user->delete();
return redirect()->back()->with('success', __('user has been removed!'));
@ -169,7 +227,7 @@ class UserController extends Controller
* @param User $user
* @return RedirectResponse
*/
public function verifyEmail(Request $request, User $user)
public function verifyEmail(User $user)
{
$user->verifyEmail();
@ -183,6 +241,8 @@ class UserController extends Controller
*/
public function loginAs(Request $request, User $user)
{
$this->checkPermission(self::LOGIN_PERMISSION);
$request->session()->put('previousUser', Auth::user()->id);
Auth::login($user);
@ -195,6 +255,7 @@ class UserController extends Controller
*/
public function logBackIn(Request $request)
{
Auth::loginUsingId($request->session()->get('previousUser'), true);
$request->session()->remove('previousUser');
@ -207,9 +268,13 @@ class UserController extends Controller
* @param User $user
* @return Application|Factory|View|Response
*/
public function notifications(User $user)
public function notifications()
{
return view('admin.users.notifications');
$this->checkPermission(self::NOTIFY_PERMISSION);
$roles = Role::all();
return view('admin.users.notifications')->with(["roles" => $roles]);
}
/**
@ -223,12 +288,16 @@ class UserController extends Controller
*/
public function notify(Request $request)
{
$this->checkPermission(self::NOTIFY_PERMISSION);
//TODO: reimplement the required validation on all,users and roles . didnt work -- required_without:users,roles
$data = $request->validate([
'via' => 'required|min:1|array',
'via.*' => 'required|string|in:mail,database',
'all' => 'required_without:users|boolean',
'users' => 'required_without:all|min:1|array',
'users.*' => 'exists:users,id',
'all' => 'boolean',
'users' => 'min:1|array',
'roles' => 'min:1|array',
'roles.*' => 'required_without:all,users|exists:roles,id',
'title' => 'required|string|min:1',
'content' => 'required|string|min:1',
]);
@ -247,8 +316,19 @@ class UserController extends Controller
->line(new HtmlString($data['content']));
}
$all = $data['all'] ?? false;
$users = $all ? User::all() : User::whereIn('id', $data['users'])->get();
Notification::send($users, new DynamicNotification($data['via'], $database, $mail));
$roles = $data['roles'] ?? false;
if(!$roles){
$users = $all ? User::all() : User::whereIn('id', $data['users'])->get();
} else{
$users = User::role($data["roles"])->get();
}
try {
Notification::send($users, new DynamicNotification($data['via'], $database, $mail));
} catch (Exception $e) {
return redirect()->route('admin.users.notifications')->with('error', __('The attempt to send the email failed with the error: ' . $e->getMessage()));
}
return redirect()->route('admin.users.notifications')->with('success', __('Notification sent!'));
}
@ -259,6 +339,12 @@ class UserController extends Controller
*/
public function toggleSuspended(User $user)
{
$this->checkPermission(self::SUSPEND_PERMISSION);
if (Auth::user()->id === $user->id) {
return redirect()->back()->with('error', __('You can not suspend yourself!'));
}
try {
!$user->isSuspended() ? $user->suspend() : $user->unSuspend();
} catch (Exception $exception) {
@ -273,17 +359,19 @@ class UserController extends Controller
*/
public function dataTable(Request $request)
{
$query = User::with('discordUser')->withCount('servers');
// manually count referrals in user_referrals table
$query->selectRaw('users.*, (SELECT COUNT(*) FROM user_referrals WHERE user_referrals.referral_id = users.id) as referrals_count');
$query = User::query()
->withCount('servers')
->leftJoin('model_has_roles', 'users.id', '=', 'model_has_roles.model_id')
->leftJoin('roles', 'model_has_roles.role_id', '=', 'roles.id')
->selectRaw('users.*, roles.name as role_name, (SELECT COUNT(*) FROM user_referrals WHERE user_referrals.referral_id = users.id) as referrals_count')
->where('model_has_roles.model_type', User::class);
return datatables($query)
->addColumn('avatar', function (User $user) {
return '<img width="28px" height="28px" class="rounded-circle ml-1" src="' . $user->getAvatar() . '">';
return '<img width="28px" height="28px" class="ml-1 rounded-circle" src="' . $user->getAvatar() . '">';
})
->addColumn('credits', function (User $user) {
return '<i class="fas fa-coins mr-2"></i> ' . $user->credits();
return '<i class="mr-2 fas fa-coins"></i> ' . $user->credits();
})
->addColumn('verified', function (User $user) {
return $user->getVerifiedStatus();
@ -297,10 +385,10 @@ class UserController extends Controller
$suspendText = $user->isSuspended() ? __('Unsuspend') : __('Suspend');
return '
<a data-content="' . __('Login as User') . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.users.loginas', $user->id) . '" class="btn btn-sm btn-primary mr-1"><i class="fas fa-sign-in-alt"></i></a>
<a data-content="' . __('Verify') . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.users.verifyEmail', $user->id) . '" class="btn btn-sm btn-secondary mr-1"><i class="fas fa-envelope"></i></a>
<a data-content="' . __('Show') . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.users.show', $user->id) . '" class="btn btn-sm text-white btn-warning mr-1"><i class="fas fa-eye"></i></a>
<a data-content="' . __('Edit') . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.users.edit', $user->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
<a data-content="' . __('Login as User') . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.users.loginas', $user->id) . '" class="mr-1 btn btn-sm btn-primary"><i class="fas fa-sign-in-alt"></i></a>
<a data-content="' . __('Verify') . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.users.verifyEmail', $user->id) . '" class="mr-1 btn btn-sm btn-secondary"><i class="fas fa-envelope"></i></a>
<a data-content="' . __('Show') . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.users.show', $user->id) . '" class="mr-1 text-white btn btn-sm btn-warning"><i class="fas fa-eye"></i></a>
<a data-content="' . __('Edit') . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.users.edit', $user->id) . '" class="mr-1 btn btn-sm btn-info"><i class="fas fa-pen"></i></a>
<form class="d-inline" method="post" action="' . route('admin.users.togglesuspend', $user->id) . '">
' . csrf_field() . '
<button data-content="' . $suspendText . '" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm ' . $suspendColor . ' text-white mr-1"><i class="far ' . $suspendIcon . '"></i></button>
@ -308,34 +396,26 @@ class UserController extends Controller
<form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.users.destroy', $user->id) . '">
' . csrf_field() . '
' . method_field('DELETE') . '
<button data-content="' . __('Delete') . '" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
<button data-content="' . __('Delete') . '" data-toggle="popover" data-trigger="hover" data-placement="top" class="mr-1 btn btn-sm btn-danger"><i class="fas fa-trash"></i></button>
</form>
';
})
->editColumn('role', function (User $user) {
switch ($user->role) {
case 'admin':
$badgeColor = 'badge-danger';
break;
case 'moderator':
$badgeColor = 'badge-info';
break;
case 'client':
$badgeColor = 'badge-success';
break;
default:
$badgeColor = 'badge-secondary';
break;
$html = '';
foreach ($user->roles as $role) {
$html .= "<span style='background-color: $role->color' class='badge'>$role->name</span>";
}
return '<span class="badge ' . $badgeColor . '">' . $user->role . '</span>';
return $html;
})
->editColumn('last_seen', function (User $user) {
return $user->last_seen ? $user->last_seen->diffForHumans() : __('Never');
})
->editColumn('name', function (User $user) {
return '<a class="text-info" target="_blank" href="' . config('SETTINGS::SYSTEM:PTERODACTYL:URL') . '/admin/users/view/' . $user->pterodactyl_id . '">' . strip_tags($user->name) . '</a>';
->editColumn('name', function (User $user, PterodactylSettings $ptero_settings) {
return '<a class="text-info" target="_blank" href="' . $ptero_settings->panel_url . '/admin/users/view/' . $user->pterodactyl_id . '">' . strip_tags($user->name) . '</a>';
})
->orderColumn('role', 'role_name $1')
->rawColumns(['avatar', 'name', 'credits', 'role', 'usage', 'actions'])
->make();
}

View file

@ -6,6 +6,8 @@ use App\Events\UserUpdateCreditsEvent;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Voucher;
use App\Settings\GeneralSettings;
use App\Settings\LocaleSettings;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
@ -17,14 +19,21 @@ use Illuminate\Validation\ValidationException;
class VoucherController extends Controller
{
const READ_PERMISSION = "admin.voucher.read";
const WRITE_PERMISSION = "admin.voucher.write";
/**
* Display a listing of the resource.
*
* @return Application|Factory|View
*/
public function index()
public function index(LocaleSettings $locale_settings, GeneralSettings $general_settings)
{
return view('admin.vouchers.index');
$this->checkPermission(self::READ_PERMISSION);
return view('admin.vouchers.index', [
'locale_datatables' => $locale_settings->datatables,
'credits_display_name' => $general_settings->credits_display_name
]);
}
/**
@ -32,9 +41,12 @@ class VoucherController extends Controller
*
* @return Application|Factory|View
*/
public function create()
public function create(GeneralSettings $general_settings)
{
return view('admin.vouchers.create');
$this->checkPermission(self::WRITE_PERMISSION);
return view('admin.vouchers.create', [
'credits_display_name' => $general_settings->credits_display_name
]);
}
/**
@ -75,10 +87,12 @@ class VoucherController extends Controller
* @param Voucher $voucher
* @return Application|Factory|View
*/
public function edit(Voucher $voucher)
public function edit(Voucher $voucher, GeneralSettings $general_settings)
{
$this->checkPermission(self::WRITE_PERMISSION);
return view('admin.vouchers.edit', [
'voucher' => $voucher,
'credits_display_name' => $general_settings->credits_display_name
]);
}
@ -112,15 +126,20 @@ class VoucherController extends Controller
*/
public function destroy(Voucher $voucher)
{
$this->checkPermission(self::WRITE_PERMISSION);
$voucher->delete();
return redirect()->back()->with('success', __('voucher has been removed!'));
}
public function users(Voucher $voucher)
public function users(Voucher $voucher, LocaleSettings $locale_settings, GeneralSettings $general_settings)
{
$this->checkPermission(self::READ_PERMISSION);
return view('admin.vouchers.users', [
'voucher' => $voucher,
'locale_datatables' => $locale_settings->datatables,
'credits_display_name' => $general_settings->credits_display_name
]);
}
@ -130,7 +149,7 @@ class VoucherController extends Controller
*
* @throws ValidationException
*/
public function redeem(Request $request)
public function redeem(Request $request, GeneralSettings $general_settings)
{
//general validations
$request->validate([
@ -161,7 +180,7 @@ class VoucherController extends Controller
if ($request->user()->credits + $voucher->credits >= 99999999) {
throw ValidationException::withMessages([
'code' => "You can't redeem this voucher because you would exceed the limit of ".CREDITS_DISPLAY_NAME,
'code' => "You can't redeem this voucher because you would exceed the limit of " . $general_settings->credits_display_name,
]);
}
@ -171,7 +190,7 @@ class VoucherController extends Controller
event(new UserUpdateCreditsEvent($request->user()));
return response()->json([
'success' => "{$voucher->credits} ".CREDITS_DISPLAY_NAME.' '.__('have been added to your balance!'),
'success' => "{$voucher->credits} ". $general_settings->credits_display_name .' '.__('have been added to your balance!'),
]);
}
@ -184,7 +203,7 @@ class VoucherController extends Controller
return '<a class="text-info" target="_blank" href="'.route('admin.users.show', $user->id).'">'.$user->name.'</a>';
})
->addColumn('credits', function (User $user) {
return '<i class="fas fa-coins mr-2"></i> '.$user->credits();
return '<i class="mr-2 fas fa-coins"></i> '.$user->credits();
})
->addColumn('last_seen', function (User $user) {
return $user->last_seen ? $user->last_seen->diffForHumans() : '';
@ -195,28 +214,33 @@ class VoucherController extends Controller
public function dataTable()
{
$query = Voucher::query();
$query = Voucher::selectRaw('
vouchers.*,
CASE
WHEN (SELECT COUNT(*) FROM user_voucher WHERE user_voucher.voucher_id = vouchers.id) >= vouchers.uses THEN "USES_LIMIT_REACHED"
WHEN vouchers.expires_at IS NOT NULL AND vouchers.expires_at < NOW() THEN "EXPIRED"
ELSE "VALID"
END as derived_status
');
return datatables($query)
->addColumn('actions', function (Voucher $voucher) {
return '
<a data-content="'.__('Users').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('admin.vouchers.users', $voucher->id).'" class="btn btn-sm btn-primary mr-1"><i class="fas fa-users"></i></a>
<a data-content="'.__('Edit').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('admin.vouchers.edit', $voucher->id).'" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
<a data-content="'.__('Users').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('admin.vouchers.users', $voucher->id).'" class="mr-1 btn btn-sm btn-primary"><i class="fas fa-users"></i></a>
<a data-content="'.__('Edit').'" data-toggle="popover" data-trigger="hover" data-placement="top" href="'.route('admin.vouchers.edit', $voucher->id).'" class="mr-1 btn btn-sm btn-info"><i class="fas fa-pen"></i></a>
<form class="d-inline" onsubmit="return submitResult();" method="post" action="'.route('admin.vouchers.destroy', $voucher->id).'">
'.csrf_field().'
'.method_field('DELETE').'
<button data-content="'.__('Delete').'" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
<button data-content="'.__('Delete').'" data-toggle="popover" data-trigger="hover" data-placement="top" class="mr-1 btn btn-sm btn-danger"><i class="fas fa-trash"></i></button>
</form>
';
})
->addColumn('status', function (Voucher $voucher) {
$color = 'success';
if ($voucher->getStatus() != __('VALID')) {
$color = 'danger';
}
$color = ($voucher->derived_status == 'VALID') ? 'success' : 'danger';
$status = str_replace('_', ' ', $voucher->derived_status);
return '<span class="badge badge-'.$color.'">'.$voucher->getStatus().'</span>';
return '<span class="badge badge-'.$color.'">'.$status.'</span>';
})
->editColumn('uses', function (Voucher $voucher) {
return "{$voucher->used} / {$voucher->uses}";
@ -226,14 +250,15 @@ class VoucherController extends Controller
})
->editColumn('expires_at', function (Voucher $voucher) {
if (! $voucher->expires_at) {
return '';
return __("Never");
}
return $voucher->expires_at ? $voucher->expires_at->diffForHumans() : '';
return $voucher->expires_at ? $voucher->expires_at->diffForHumans() : __("Never");
})
->editColumn('code', function (Voucher $voucher) {
return "<code>{$voucher->code}</code>";
})
->orderColumn('status', 'derived_status $1')
->rawColumns(['actions', 'code', 'status'])
->make();
}

View file

@ -14,6 +14,7 @@ use Illuminate\Support\Facades\Notification;
use Illuminate\Support\HtmlString;
use Illuminate\Validation\ValidationException;
use Spatie\ValidationRules\Rules\Delimited;
use Exception;
class NotificationController extends Controller
{
@ -104,8 +105,12 @@ class NotificationController extends Controller
'users' => ['No users found!'],
]);
}
Notification::send($users, new DynamicNotification($via, $database, $mail));
try {
Notification::send($users, new DynamicNotification($via, $database, $mail));
}
catch (Exception $e) {
return response()->json(['message' => 'The attempt to send the email failed with the error: ' . $e->getMessage()], 500);
}
return response()->json(['message' => 'Notification successfully sent.', 'user_count' => $users->count()]);
}

View file

@ -0,0 +1,160 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Validation\Rule;
use Spatie\Permission\Models\Role;
use Spatie\QueryBuilder\QueryBuilder;
class RoleController extends Controller
{
const ALLOWED_INCLUDES = ['permissions', 'users'];
const ALLOWED_FILTERS = ['name'];
/**
* Display a listing of the resource.
*
* @return LengthAwarePaginator
*/
public function index(Request $request)
{
$query = QueryBuilder::for(Role::class)
->allowedIncludes(self::ALLOWED_INCLUDES)
->allowedFilters(self::ALLOWED_FILTERS);
return $query->paginate($request->input('per_page') ?? 50);
}
/**
* Show the form for creating a new resource.
*
* @return Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:191',
'color' => [
'required',
'regex:/^#([a-f0-9]{6}|[a-f0-9]{3})$/i'
],
'power' => 'required',
]);
$role = Role::create([
'name' => $request->name,
'color' => $request->color,
'power' => $request->power,
]);
if ($request->permissions) {
$permissions = explode(",",$request->permissions);
foreach($permissions as $permission){
$role->givePermissionTo($permission);
}
}
return $role;
}
/**
* Display the specified resource.
*
* @param int $id
* @return Role|Collection|Model
*/
public function show(int $id)
{
$query = QueryBuilder::for(Role::class)
->where('id', '=', $id)
->allowedIncludes(self::ALLOWED_INCLUDES);
return $query->firstOrFail();
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param Request $request
* @param int $id
* @return Response
*/
public function update(Request $request, int $id)
{
$role = Role::findOrFail($id);
$request->validate([
'name' => 'sometimes|string|max:191',
'color' => [
'sometimes',
'regex:/^#([a-f0-9]{6}|[a-f0-9]{3})$/i'
],
'power' => 'sometimes',
]);
if ($request->permissions) {
$permissions = explode(",",$request->permissions);
$role->syncPermissions($permissions);
}
$role->update($request->except('permissions'));
return $role;
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return Response
*/
public function destroy(int $id)
{
$role = Role::findOrFail($id);
if($role->id == 1 || $role->id == 3|| $role->id == 4){ //cannot delete admin and User role
return response()->json([
'error' => 'Not allowed to delete Admin, Client or Member'], 400);
}
$users = User::role($role)->get();
foreach($users as $user){
$user->syncRoles([4]);
}
$role->delete();
return $role;
}
}

View file

@ -8,7 +8,7 @@ use App\Http\Controllers\Controller;
use App\Models\DiscordUser;
use App\Models\User;
use App\Notifications\ReferralNotification;
use App\Traits\Referral;
use App\Settings\UserSettings;
use Carbon\Carbon;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
@ -28,11 +28,9 @@ use Spatie\QueryBuilder\QueryBuilder;
class UserController extends Controller
{
use Referral;
const ALLOWED_INCLUDES = ['servers', 'notifications', 'payments', 'vouchers', 'roles', 'discordUser'];
const ALLOWED_INCLUDES = ['servers', 'notifications', 'payments', 'vouchers', 'discordUser'];
const ALLOWED_FILTERS = ['name', 'server_limit', 'email', 'pterodactyl_id', 'role', 'suspended'];
const ALLOWED_FILTERS = ['name', 'server_limit', 'email', 'pterodactyl_id', 'suspended'];
/**
* Display a listing of the resource.
@ -88,14 +86,13 @@ class UserController extends Controller
'email' => 'sometimes|string|email',
'credits' => 'sometimes|numeric|min:0|max:1000000',
'server_limit' => 'sometimes|numeric|min:0|max:1000000',
'role' => ['sometimes', Rule::in(['admin', 'moderator', 'client', 'member'])],
]);
event(new UserUpdateCreditsEvent($user));
//Update Users Password on Pterodactyl
//Username,Mail,First and Lastname are required aswell
$response = Pterodactyl::client()->patch('/application/users/' . $user->pterodactyl_id, [
$response = Pterodactyl::client()->patch('/application/users/'.$user->pterodactyl_id, [
'username' => $request->name,
'first_name' => $request->name,
'last_name' => $request->name,
@ -108,7 +105,10 @@ class UserController extends Controller
'pterodactyl_error_status' => $response->toException()->getCode(),
]);
}
$user->update($request->all());
if($request->has("role")){
$user->syncRoles($request->role);
}
$user->update($request->except('role'));
return $user;
}
@ -232,7 +232,7 @@ class UserController extends Controller
$discordUser = DiscordUser::find($id);
$user = $discordUser ? $discordUser->user : User::findOrFail($id);
if (!$user->isSuspended()) {
if (! $user->isSuspended()) {
throw ValidationException::withMessages([
'error' => 'You cannot unsuspend an User who is not suspended.',
]);
@ -243,10 +243,25 @@ class UserController extends Controller
return $user;
}
/**
* Create a unique Referral Code for User
*
* @return string
*/
protected function createReferralCode()
{
$referralcode = STR::random(8);
if (User::where('referral_code', '=', $referralcode)->exists()) {
$this->createReferralCode();
}
return $referralcode;
}
/**
* @throws ValidationException
*/
public function store(Request $request)
public function store(Request $request, UserSettings $userSettings)
{
$request->validate([
'name' => ['required', 'string', 'max:30', 'min:4', 'alpha_num', 'unique:users'],
@ -255,7 +270,7 @@ class UserController extends Controller
]);
// Prevent the creation of new users via API if this is enabled.
if (!config('SETTINGS::SYSTEM:CREATION_OF_NEW_USERS', 'true')) {
if (! $userSettings->creation_enabled) {
throw ValidationException::withMessages([
'error' => 'The creation of new users has been blocked by the system administrator.',
]);
@ -293,7 +308,7 @@ class UserController extends Controller
'pterodactyl_id' => $response->json()['attributes']['id'],
]);
//INCREMENT REFERRAL-USER CREDITS
if (!empty($request->input('referral_code'))) {
if (! empty($request->input('referral_code'))) {
$ref_code = $request->input('referral_code');
$new_user = $user->id;
if ($ref_user = User::query()->where('referral_code', '=', $ref_code)->first()) {

View file

@ -3,6 +3,7 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Settings\GeneralSettings;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
@ -31,13 +32,13 @@ class ForgotPasswordController extends Controller
$this->middleware('guest');
}
protected function validateEmail(Request $request)
protected function validateEmail(Request $request, GeneralSettings $general_settings)
{
$this->validate($request, [
'email' => ['required', 'string', 'email', 'max:255'],
]);
if (config('SETTINGS::RECAPTCHA:ENABLED') == 'true') {
if ($general_settings->recaptcha_enabled) {
$this->validate($request, [
'g-recaptcha-response' => 'required|recaptcha',
]);

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use App\Settings\GeneralSettings;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -53,13 +54,13 @@ class LoginController extends Controller
return $field;
}
public function login(Request $request)
public function login(Request $request, GeneralSettings $general_settings)
{
$validationRules = [
$this->username() => 'required|string',
'password' => 'required|string',
];
if (config('SETTINGS::RECAPTCHA:ENABLED') == 'true') {
if ($general_settings->recaptcha_enabled) {
$validationRules['g-recaptcha-response'] = ['required', 'recaptcha'];
}
$request->validate($validationRules);

View file

@ -2,13 +2,18 @@
namespace App\Http\Controllers\Auth;
use App\Classes\Pterodactyl;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Notifications\ReferralNotification;
use App\Providers\RouteServiceProvider;
use App\Traits\Referral;
use Carbon\Carbon;
use App\Settings\PterodactylSettings;
use App\Classes\PterodactylClient;
use App\Settings\GeneralSettings;
use App\Settings\ReferralSettings;
use App\Settings\UserSettings;
use App\Settings\WebsiteSettings;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
@ -17,9 +22,28 @@ use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Spatie\Permission\Models\Role;
class RegisterController extends Controller
{
private $pterodactyl;
private $credits_display_name;
private $recaptcha_enabled;
private $website_show_tos;
private $register_ip_check;
private $initial_credits;
private $initial_server_limit;
private $referral_mode;
private $referral_reward;
/*
|--------------------------------------------------------------------------
| Register Controller
@ -45,9 +69,18 @@ class RegisterController extends Controller
*
* @return void
*/
public function __construct()
public function __construct(PterodactylSettings $ptero_settings, GeneralSettings $general_settings, WebsiteSettings $website_settings, UserSettings $user_settings, ReferralSettings $referral_settings)
{
$this->middleware('guest');
$this->pterodactyl = new PterodactylClient($ptero_settings);
$this->credits_display_name = $general_settings->credits_display_name;
$this->recaptcha_enabled = $general_settings->recaptcha_enabled;
$this->website_show_tos = $website_settings->show_tos;
$this->register_ip_check = $user_settings->register_ip_check;
$this->initial_credits = $user_settings->initial_credits;
$this->initial_server_limit = $user_settings->initial_server_limit;
$this->referral_mode = $referral_settings->mode;
$this->referral_reward = $referral_settings->reward;
}
/**
@ -63,14 +96,14 @@ class RegisterController extends Controller
'email' => ['required', 'string', 'email', 'max:64', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
];
if (config('SETTINGS::RECAPTCHA:ENABLED') == 'true') {
if ($this->recaptcha_enabled) {
$validationRules['g-recaptcha-response'] = ['required', 'recaptcha'];
}
if (config('SETTINGS::SYSTEM:SHOW_TOS') == 'true') {
if ($this->website_show_tos) {
$validationRules['terms'] = ['required'];
}
if (config('SETTINGS::SYSTEM:REGISTER_IP_CHECK', 'true') == 'true') {
if ($this->register_ip_check) {
//check if ip has already made an account
$data['ip'] = session()->get('ip') ?? request()->ip();
@ -99,15 +132,18 @@ class RegisterController extends Controller
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'credits' => config('SETTINGS::USER:INITIAL_CREDITS', 150),
'server_limit' => config('SETTINGS::USER:INITIAL_SERVER_LIMIT', 1),
'credits' => $this->initial_credits,
'server_limit' => $this->initial_server_limit,
'password' => Hash::make($data['password']),
'referral_code' => $this->createReferralCode(),
'pterodactyl_id' => Str::uuid(),
]);
$response = Pterodactyl::client()->post('/application/users', [
'external_id' => App::environment('local') ? Str::random(16) : (string) $user->id,
$user->syncRoles(Role::findByName('User'));
$response = $this->pterodactyl->application->post('/application/users', [
'external_id' => null,
'username' => $user->name,
'email' => $user->email,
'first_name' => $user->name,
@ -117,6 +153,10 @@ class RegisterController extends Controller
'language' => 'en',
]);
$user->update([
'pterodactyl_id' => $response->json()['attributes']['id'],
]);
if ($response->failed()) {
$user->delete();
Log::error('Pterodactyl Registration Error: ' . $response->json()['errors'][0]['detail']);
@ -125,24 +165,23 @@ class RegisterController extends Controller
]);
}
$user->update([
'pterodactyl_id' => $response->json()['attributes']['id'],
]);
// delete activity log for user creation where description = 'created' or 'deleted' and subject_id = user_id
DB::table('activity_log')->where('description', 'created')->orWhere('description', 'deleted')->where('subject_id', $user->id)->delete();
//INCREMENT REFERRAL-USER CREDITS
if (!empty($data['referral_code'])) {
$ref_code = $data['referral_code'];
$new_user = $user->id;
if ($ref_user = User::query()->where('referral_code', '=', $ref_code)->first()) {
if (config('SETTINGS::REFERRAL:MODE') == 'sign-up' || config('SETTINGS::REFERRAL:MODE') == 'both') {
$ref_user->increment('credits', config('SETTINGS::REFERRAL::REWARD'));
if ($this->referral_mode === 'sign-up' || $this->referral_mode === 'both') {
$ref_user->increment('credits', $this->referral_reward);
$ref_user->notify(new ReferralNotification($ref_user->id, $new_user));
//LOGS REFERRALS IN THE ACTIVITY LOG
activity()
->performedOn($user)
->causedBy($ref_user)
->log('gained ' . config('SETTINGS::REFERRAL::REWARD') . ' ' . config('SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME') . ' for sign-up-referral of ' . $user->name . ' (ID:' . $user->id . ')');
->log('gained ' . $this->referral_reward . ' ' . $this->credits_display_name . ' for sign-up-referral of ' . $user->name . ' (ID:' . $user->id . ')');
}
//INSERT INTO USER_REFERRALS TABLE
DB::table('user_referrals')->insert([

View file

@ -5,22 +5,24 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\DiscordUser;
use App\Models\User;
use App\Settings\DiscordSettings;
use App\Settings\UserSettings;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Laravel\Socialite\Facades\Socialite;
class SocialiteController extends Controller
{
public function redirect()
public function redirect(DiscordSettings $discord_settings)
{
$scopes = ! empty(config('SETTINGS::DISCORD:BOT_TOKEN')) && ! empty(config('SETTINGS::DISCORD:GUILD_ID')) ? ['guilds.join'] : [];
$scopes = !empty($discord_settings->bot_token) && !empty($discord_settings->guild_id) ? ['guilds.join'] : [];
return Socialite::driver('discord')
->scopes($scopes)
->redirect();
}
public function callback()
public function callback(DiscordSettings $discord_settings, UserSettings $user_settings)
{
if (Auth::guest()) {
return abort(500);
@ -29,9 +31,9 @@ class SocialiteController extends Controller
/** @var User $user */
$user = Auth::user();
$discord = Socialite::driver('discord')->user();
$botToken = config('SETTINGS::DISCORD:BOT_TOKEN');
$guildId = config('SETTINGS::DISCORD:GUILD_ID');
$roleId = config('SETTINGS::DISCORD:ROLE_ID');
$botToken = $discord_settings->bot_token;
$guildId = $discord_settings->guild_id;
$roleId = $discord_settings->role_id;
//save / update discord_users
@ -49,8 +51,8 @@ class SocialiteController extends Controller
DiscordUser::create(array_merge($discord->user, ['user_id' => Auth::user()->id]));
//update user
Auth::user()->increment('credits', config('SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_DISCORD'));
Auth::user()->increment('server_limit', config('SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD'));
Auth::user()->increment('credits', $user_settings->credits_reward_after_verify_discord);
Auth::user()->increment('server_limit', $user_settings->server_limit_after_verify_discord);
Auth::user()->update(['discord_verified_at' => now()]);
} else {
$user->discordUser->update($discord->user);

View file

@ -2,12 +2,44 @@
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Auth;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
/**
* Check if user has permissions
* Abort 403 if the user doesn't have the required permission
*
* @param string $permission
* @return void
*/
public function checkPermission(string $permission)
{
/** @var User $user */
$user = Auth::user();
if (!$user->can($permission)) {
abort(403, __('User does not have the right permissions.'));
}
}
/**
* Check if user has permissions
*
* @param string $permission
* @return bool
*/
public function can(string $permission): bool
{
/** @var User $user */
$user = Auth::user();
return $user->can($permission);
}
}

View file

@ -4,7 +4,9 @@ namespace App\Http\Controllers;
use App\Models\PartnerDiscount;
use App\Models\UsefulLink;
use Illuminate\Http\Request;
use App\Settings\GeneralSettings;
use App\Settings\WebsiteSettings;
use App\Settings\ReferralSettings;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
@ -32,7 +34,7 @@ class HomeController extends Controller
if (Storage::exists('callHome')) {
return;
}
Http::asForm()->post('https://market.ctrlpanel.gg/callhome.php', [
Http::asForm()->post('https://market.CtrlPanel.gg/callhome.php', [
'id' => Hash::make(URL::current()),
]);
Storage::put('callHome', 'This is only used to count the installations of cpgg.');
@ -89,7 +91,7 @@ class HomeController extends Controller
}
/** Show the application dashboard. */
public function index(Request $request)
public function index(GeneralSettings $general_settings, WebsiteSettings $website_settings, ReferralSettings $referral_settings)
{
$usage = Auth::user()->creditUsage();
$credits = Auth::user()->Credits();
@ -99,7 +101,7 @@ class HomeController extends Controller
/** Build our Time-Left-Box */
if ($credits > 0.01 and $usage > 0) {
$daysLeft = number_format(($credits * 30) / $usage, 2, '.', '');
$daysLeft = number_format($credits / ($usage / 30), 2, '.', '');
$hoursLeft = number_format($credits / ($usage / 30 / 24), 2, '.', '');
$bg = $this->getTimeLeftBoxBackground($daysLeft);
@ -120,6 +122,9 @@ class HomeController extends Controller
'numberOfReferrals' => DB::table('user_referrals')->where('referral_id', '=', Auth::user()->id)->count(),
'partnerDiscount' => PartnerDiscount::where('user_id', Auth::user()->id)->first(),
'myDiscount' => PartnerDiscount::getDiscount(),
'general_settings' => $general_settings,
'website_settings' => $website_settings,
'referral_settings' => $referral_settings
]);
}
}

View file

@ -2,18 +2,26 @@
namespace App\Http\Controllers;
use App\Classes\Pterodactyl;
use App\Models\Egg;
use App\Models\Location;
use App\Models\Node;
use App\Classes\PterodactylClient;
use App\Models\Pterodactyl\Egg;
use App\Models\Pterodactyl\Location;
use App\Models\Pterodactyl\Node;
use App\Models\Product;
use App\Settings\PterodactylSettings;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
class ProductController extends Controller
{
{
private $pterodactyl;
public function __construct(PterodactylSettings $ptero_settings)
{
$this->pterodactyl = new PterodactylClient($ptero_settings);
}
/**
* @description get product locations based on selected egg
*
@ -60,7 +68,7 @@ class ProductController extends Controller
{
$nodes = $this->getNodesBasedOnEgg($request, $egg);
foreach ($nodes as $key => $node) {
$pteroNode = Pterodactyl::getNode($node->id);
$pteroNode = $this->pterodactyl->getNode($node->id);
if ($pteroNode['allocated_resources']['memory'] >= ($pteroNode['memory'] * ($pteroNode['memory_overallocate'] + 100) / 100) || $pteroNode['allocated_resources']['disk'] >= ($pteroNode['disk'] * ($pteroNode['disk_overallocate'] + 100) / 100)) {
$nodes->forget($key);
}
@ -109,7 +117,7 @@ class ProductController extends Controller
})
->get();
$pteroNode = Pterodactyl::getNode($node->id);
$pteroNode = $this->pterodactyl->getNode($node->id);
foreach ($products as $key => $product) {
if ($product->memory > ($pteroNode['memory'] * ($pteroNode['memory_overallocate'] + 100) / 100) - $pteroNode['allocated_resources']['memory'] || $product->disk > ($pteroNode['disk'] * ($pteroNode['disk_overallocate'] + 100) / 100) - $pteroNode['allocated_resources']['disk']) {
$product->doesNotFit = true;

View file

@ -2,8 +2,12 @@
namespace App\Http\Controllers;
use App\Classes\Pterodactyl;
use App\Models\User;
use App\Settings\UserSettings;
use App\Settings\PterodactylSettings;
use App\Classes\PterodactylClient;
use App\Settings\DiscordSettings;
use App\Settings\ReferralSettings;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -12,37 +16,32 @@ use Illuminate\Validation\ValidationException;
class ProfileController extends Controller
{
/** Display a listing of the resource. */
public function index()
private $pterodactyl;
public function __construct(PterodactylSettings $ptero_settings)
{
$this->pterodactyl = new PterodactylClient($ptero_settings);
}
/** Display a listing of the resource. */
public function index(UserSettings $user_settings, DiscordSettings $discord_settings, ReferralSettings $referral_settings)
{
switch (Auth::user()->role) {
case 'admin':
$badgeColor = 'badge-danger';
break;
case 'mod':
$badgeColor = 'badge-info';
break;
case 'client':
$badgeColor = 'badge-success';
break;
default:
$badgeColor = 'badge-secondary';
break;
}
return view('profile.index')->with([
'user' => Auth::user(),
'credits_reward_after_verify_discord' => config('SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_DISCORD'),
'force_email_verification' => config('SETTINGS::USER:FORCE_EMAIL_VERIFICATION'),
'force_discord_verification' => config('SETTINGS::USER:FORCE_DISCORD_VERIFICATION'),
'badgeColor' => $badgeColor,
'credits_reward_after_verify_discord' => $user_settings->credits_reward_after_verify_discord,
'force_email_verification' => $user_settings->force_email_verification,
'force_discord_verification' => $user_settings->force_discord_verification,
'discord_client_id' => $discord_settings->client_id,
'discord_client_secret' => $discord_settings->client_secret,
'referral_enabled' => $referral_settings->enabled
]);
}
public function selfDestroyUser()
{
$user = Auth::user();
if ($user->role == "admin") return back()->with("error", "You cannot delete yourself as an admin!");
if ($user->hasRole("Admin")) return back()->with("error", "You cannot delete yourself as an admin!");
$user->delete();
@ -63,15 +62,15 @@ class ProfileController extends Controller
$user = User::findOrFail($id);
//update password if necessary
if (! is_null($request->input('new_password'))) {
if (!is_null($request->input('new_password'))) {
//validate password request
$request->validate([
'current_password' => [
'required',
function ($attribute, $value, $fail) use ($user) {
if (! Hash::check($value, $user->password)) {
$fail('The '.$attribute.' is invalid.');
if (!Hash::check($value, $user->password)) {
$fail('The ' . $attribute . ' is invalid.');
}
},
],
@ -81,7 +80,7 @@ class ProfileController extends Controller
//Update Users Password on Pterodactyl
//Username,Mail,First and Lastname are required aswell
$response = Pterodactyl::client()->patch('/application/users/'.$user->pterodactyl_id, [
$response = $this->pterodactyl->application->patch('/application/users/' . $user->pterodactyl_id, [
'password' => $request->input('new_password'),
'username' => $request->input('name'),
'first_name' => $request->input('name'),
@ -103,13 +102,13 @@ class ProfileController extends Controller
//validate request
$request->validate([
'name' => 'required|min:4|max:30|alpha_num|unique:users,name,'.$id.',id',
'email' => 'required|email|max:64|unique:users,email,'.$id.',id',
'name' => 'required|min:4|max:30|alpha_num|unique:users,name,' . $id . ',id',
'email' => 'required|email|max:64|unique:users,email,' . $id . ',id',
'avatar' => 'nullable',
]);
//update avatar
if (! is_null($request->input('avatar'))) {
if (!is_null($request->input('avatar'))) {
$avatar = json_decode($request->input('avatar'));
if ($avatar->input->size > 3000000) {
abort(500);
@ -125,7 +124,7 @@ class ProfileController extends Controller
}
//update name and email on Pterodactyl
$response = Pterodactyl::client()->patch('/application/users/'.$user->pterodactyl_id, [
$response = $this->pterodactyl->application->patch('/application/users/' . $user->pterodactyl_id, [
'username' => $request->input('name'),
'first_name' => $request->input('name'),
'last_name' => $request->input('name'),

View file

@ -2,14 +2,19 @@
namespace App\Http\Controllers;
use App\Classes\Pterodactyl;
use App\Models\Egg;
use App\Models\Location;
use App\Models\Nest;
use App\Models\Node;
use App\Models\Pterodactyl\Egg;
use App\Models\Pterodactyl\Location;
use App\Models\Pterodactyl\Nest;
use App\Models\Pterodactyl\Node;
use App\Models\Product;
use App\Models\Server;
use App\Notifications\ServerCreationError;
use Carbon\Carbon;
use App\Settings\UserSettings;
use App\Settings\ServerSettings;
use App\Settings\PterodactylSettings;
use App\Classes\PterodactylClient;
use App\Settings\GeneralSettings;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Client\Response;
@ -20,8 +25,18 @@ use Illuminate\Support\Facades\Request as FacadesRequest;
class ServerController extends Controller
{
const CREATE_PERMISSION = 'user.server.create';
const UPGRADE_PERMISSION = 'user.server.upgrade';
private $pterodactyl;
public function __construct(PterodactylSettings $ptero_settings)
{
$this->pterodactyl = new PterodactylClient($ptero_settings);
}
/** Display a listing of the resource. */
public function index()
public function index(GeneralSettings $general_settings, PterodactylSettings $ptero_settings)
{
$servers = Auth::user()->servers;
@ -29,8 +44,8 @@ class ServerController extends Controller
foreach ($servers as $server) {
//Get server infos from ptero
$serverAttributes = Pterodactyl::getServerAttributes($server->pterodactyl_id, true);
if (! $serverAttributes) {
$serverAttributes = $this->pterodactyl->getServerAttributes($server->pterodactyl_id);
if (!$serverAttributes) {
continue;
}
$serverRelationships = $serverAttributes['relationships'];
@ -61,14 +76,21 @@ class ServerController extends Controller
return view('servers.index')->with([
'servers' => $servers,
'credits_display_name' => $general_settings->credits_display_name,
'pterodactyl_url' => $ptero_settings->panel_url,
'phpmyadmin_url' => $general_settings->phpmyadmin_url
]);
}
/** Show the form for creating a new resource. */
public function create()
public function create(UserSettings $user_settings, ServerSettings $server_settings, GeneralSettings $general_settings)
{
if (! is_null($this->validateConfigurationRules())) {
return $this->validateConfigurationRules();
$this->checkPermission(self::CREATE_PERMISSION);
$validate_configuration = $this->validateConfigurationRules($user_settings, $server_settings, $general_settings);
if (!is_null($validate_configuration)) {
return $validate_configuration;
}
$productCount = Product::query()->where('disabled', '=', false)->count();
@ -98,13 +120,17 @@ class ServerController extends Controller
'locations' => $locations,
'eggs' => $eggs,
'user' => Auth::user(),
'server_creation_enabled' => $server_settings->creation_enabled,
'min_credits_to_make_server' => $user_settings->min_credits_to_make_server,
'credits_display_name' => $general_settings->credits_display_name,
'store_enabled' => $general_settings->store_enabled
]);
}
/**
* @return null|RedirectResponse
*/
private function validateConfigurationRules()
private function validateConfigurationRules(UserSettings $user_settings, ServerSettings $server_settings, GeneralSettings $generalSettings)
{
//limit validation
if (Auth::user()->servers()->count() >= Auth::user()->server_limit) {
@ -120,35 +146,31 @@ class ServerController extends Controller
$nodeName = $node->name;
// Check if node has enough memory and disk space
$checkResponse = Pterodactyl::checkNodeResources($node, $product->memory, $product->disk);
$checkResponse = $this->pterodactyl->checkNodeResources($node, $product->memory, $product->disk);
if ($checkResponse == false) {
return redirect()->route('servers.index')->with('error', __("The node '".$nodeName."' doesn't have the required memory or disk left to allocate this product."));
return redirect()->route('servers.index')->with('error', __("The node '" . $nodeName . "' doesn't have the required memory or disk left to allocate this product."));
}
// Min. Credits
if (
Auth::user()->credits <
($product->minimum_credits == -1
? config('SETTINGS::USER:MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER', 50)
: $product->minimum_credits)
) {
return redirect()->route('servers.index')->with('error', 'You do not have the required amount of '.CREDITS_DISPLAY_NAME.' to use this product!');
if (Auth::user()->credits < ($product->minimum_credits == -1
? $user_settings->min_credits_to_make_server
: $product->minimum_credits)) {
return redirect()->route('servers.index')->with('error', 'You do not have the required amount of ' . $generalSettings->credits_display_name . ' to use this product!');
}
}
//Required Verification for creating an server
if (config('SETTINGS::USER:FORCE_EMAIL_VERIFICATION', 'false') === 'true' && ! Auth::user()->hasVerifiedEmail()) {
if ($user_settings->force_email_verification && !Auth::user()->hasVerifiedEmail()) {
return redirect()->route('profile.index')->with('error', __('You are required to verify your email address before you can create a server.'));
}
//Required Verification for creating an server
if (! config('SETTINGS::SYSTEM:CREATION_OF_NEW_SERVERS', 'true') && Auth::user()->role != 'admin') {
if (!$server_settings->creation_enabled && Auth::user()->cannot("admin.servers.bypass_creation_enabled")) {
return redirect()->route('servers.index')->with('error', __('The system administrator has blocked the creation of new servers.'));
}
//Required Verification for creating an server
if (config('SETTINGS::USER:FORCE_DISCORD_VERIFICATION', 'false') === 'true' && ! Auth::user()->discordUser) {
if ($user_settings->force_discord_verification && !Auth::user()->discordUser) {
return redirect()->route('profile.index')->with('error', __('You are required to link your discord account before you can create a server.'));
}
@ -156,13 +178,15 @@ class ServerController extends Controller
}
/** Store a newly created resource in storage. */
public function store(Request $request)
public function store(Request $request, UserSettings $user_settings, ServerSettings $server_settings, GeneralSettings $generalSettings)
{
/** @var Node $node */
/** @var Egg $egg */
/** @var Product $product */
if (! is_null($this->validateConfigurationRules())) {
return $this->validateConfigurationRules();
$validate_configuration = $this->validateConfigurationRules($user_settings, $server_settings, $generalSettings);
if (!is_null($validate_configuration)) {
return $validate_configuration;
}
$request->validate([
@ -180,16 +204,17 @@ class ServerController extends Controller
$server = $request->user()->servers()->create([
'name' => $request->input('name'),
'product_id' => $request->input('product'),
'last_billed' => Carbon::now()->toDateTimeString(),
]);
//get free allocation ID
$allocationId = Pterodactyl::getFreeAllocationId($node);
if (! $allocationId) {
$allocationId = $this->pterodactyl->getFreeAllocationId($node);
if (!$allocationId) {
return $this->noAllocationsError($server);
}
//create server on pterodactyl
$response = Pterodactyl::createServer($server, $egg, $allocationId);
$response = $this->pterodactyl->createServer($server, $egg, $allocationId);
if ($response->failed()) {
return $this->serverCreationFailed($response, $server);
}
@ -201,11 +226,8 @@ class ServerController extends Controller
'identifier' => $serverAttributes['identifier'],
]);
if (config('SETTINGS::SYSTEM:SERVER_CREATE_CHARGE_FIRST_HOUR', 'true') == 'true') {
if ($request->user()->credits >= $server->product->getHourlyPrice()) {
$request->user()->decrement('credits', $server->product->getHourlyPrice());
}
}
// Charge first billing cycle
$request->user()->decrement('credits', $server->product->price);
return redirect()->route('servers.index')->with('success', __('Server created'));
}
@ -234,8 +256,6 @@ class ServerController extends Controller
*/
private function serverCreationFailed(Response $response, Server $server)
{
$server->delete();
return redirect()->route('servers.index')->with('error', json_encode($response->json()));
}
@ -247,17 +267,33 @@ class ServerController extends Controller
return redirect()->route('servers.index')->with('success', __('Server removed'));
} catch (Exception $e) {
return redirect()->route('servers.index')->with('error', __('An exception has occurred while trying to remove a resource "').$e->getMessage().'"');
return redirect()->route('servers.index')->with('error', __('An exception has occurred while trying to remove a resource"') . $e->getMessage() . '"');
}
}
/** Cancel Server */
public function cancel(Server $server)
{
if ($server->user_id != Auth::user()->id) {
return back()->with('error', __('This is not your Server!'));
}
try {
$server->update([
'canceled' => now(),
]);
return redirect()->route('servers.index')->with('success', __('Server canceled'));
} catch (Exception $e) {
return redirect()->route('servers.index')->with('error', __('An exception has occurred while trying to cancel the server"') . $e->getMessage() . '"');
}
}
/** Show Server Settings */
public function show(Server $server)
public function show(Server $server, ServerSettings $server_settings, GeneralSettings $general_settings)
{
if ($server->user_id != Auth::user()->id) {
return back()->with('error', __('´This is not your Server!'));
return back()->with('error', __('This is not your Server!'));
}
$serverAttributes = Pterodactyl::getServerAttributes($server->pterodactyl_id);
$serverAttributes = $this->pterodactyl->getServerAttributes($server->pterodactyl_id);
$serverRelationships = $serverAttributes['relationships'];
$serverLocationAttributes = $serverRelationships['location']['attributes'];
@ -273,13 +309,13 @@ class ServerController extends Controller
$server->name = $serverAttributes['name'];
$server->egg = $serverRelationships['egg']['attributes']['name'];
$pteroNode = Pterodactyl::getNode($serverRelationships['node']['attributes']['id']);
$pteroNode = $this->pterodactyl->getNode($serverRelationships['node']['attributes']['id']);
$products = Product::orderBy('created_at')
->whereHas('nodes', function (Builder $builder) use ($serverRelationships) { //Only show products for that node
$builder->where('id', '=', $serverRelationships['node']['attributes']['id']);
})
->get();
->whereHas('nodes', function (Builder $builder) use ($serverRelationships) { //Only show products for that node
$builder->where('id', '=', $serverRelationships['node']['attributes']['id']);
})
->get();
// Set the each product eggs array to just contain the eggs name
foreach ($products as $product) {
@ -292,21 +328,25 @@ class ServerController extends Controller
return view('servers.settings')->with([
'server' => $server,
'products' => $products,
'server_enable_upgrade' => $server_settings->enable_upgrade,
'credits_display_name' => $general_settings->credits_display_name
]);
}
public function upgrade(Server $server, Request $request)
{
$this->checkPermission(self::UPGRADE_PERMISSION);
if ($server->user_id != Auth::user()->id) {
return redirect()->route('servers.index');
}
if (! isset($request->product_upgrade)) {
if (!isset($request->product_upgrade)) {
return redirect()->route('servers.show', ['server' => $server->id])->with('error', __('this product is the only one'));
}
$user = Auth::user();
$oldProduct = Product::where('id', $server->product->id)->first();
$newProduct = Product::where('id', $request->product_upgrade)->first();
$serverAttributes = Pterodactyl::getServerAttributes($server->pterodactyl_id);
$serverAttributes = $this->pterodactyl->getServerAttributes($server->pterodactyl_id);
$serverRelationships = $serverAttributes['relationships'];
// Get node resource allocation info
@ -317,31 +357,56 @@ class ServerController extends Controller
// Check if node has enough memory and disk space
$requireMemory = $newProduct->memory - $oldProduct->memory;
$requiredisk = $newProduct->disk - $oldProduct->disk;
$checkResponse = Pterodactyl::checkNodeResources($node, $requireMemory, $requiredisk);
$checkResponse = $this->pterodactyl->checkNodeResources($node, $requireMemory, $requiredisk);
if ($checkResponse == false) {
return redirect()->route('servers.index')->with('error', __("The node '".$nodeName."' doesn't have the required memory or disk left to upgrade the server."));
return redirect()->route('servers.index')->with('error', __("The node '" . $nodeName . "' doesn't have the required memory or disk left to upgrade the server."));
}
$priceupgrade = $newProduct->getHourlyPrice();
// calculate the amount of credits that the user overpayed for the old product when canceling the server right now
// billing periods are hourly, daily, weekly, monthly, quarterly, half-annually, annually
$billingPeriod = $oldProduct->billing_period;
// seconds
$billingPeriods = [
'hourly' => 3600,
'daily' => 86400,
'weekly' => 604800,
'monthly' => 2592000,
'quarterly' => 7776000,
'half-annually' => 15552000,
'annually' => 31104000
];
// Get the amount of hours the user has been using the server
$billingPeriodMultiplier = $billingPeriods[$billingPeriod];
$timeDifference = now()->diffInSeconds($server->last_billed);
if ($priceupgrade < $oldProduct->getHourlyPrice()) {
$priceupgrade = 0;
}
if ($user->credits >= $priceupgrade && $user->credits >= $newProduct->minimum_credits) {
$server->product_id = $request->product_upgrade;
$server->update();
// Calculate the price for the time the user has been using the server
$overpayedCredits = $oldProduct->price - $oldProduct->price * ($timeDifference / $billingPeriodMultiplier);
if ($user->credits >= $newProduct->price && $user->credits >= $newProduct->minimum_credits) {
$server->allocation = $serverAttributes['allocation'];
$response = Pterodactyl::updateServer($server, $newProduct);
if ($response->failed()) {
return $this->serverCreationFailed($response, $server);
}
//update user balance
$user->decrement('credits', $priceupgrade);
$response = $this->pterodactyl->updateServer($server, $newProduct);
if ($response->failed()) return redirect()->route('servers.index')->with('error', __("The system was unable to update your server product. Please try again later or contact support."));
//restart the server
$response = Pterodactyl::powerAction($server, 'restart');
if ($response->failed()) {
return redirect()->route('servers.index')->with('error', $response->json()['errors'][0]['detail']);
}
$response = $this->pterodactyl->powerAction($server, 'restart');
if ($response->failed()) return redirect()->route('servers.index')->with('error', 'Upgrade Failed! Could not restart the server: ' . $response->json()['errors'][0]['detail']);
// Remove the allocation property from the server object as it is not a column in the database
unset($server->allocation);
// Update the server on controlpanel
$server->update([
'product_id' => $newProduct->id,
'updated_at' => now(),
'last_billed' => now(),
'canceled' => null,
]);
// Refund the user the overpayed credits
if ($overpayedCredits > 0) $user->increment('credits', $overpayedCredits);
// Withdraw the credits for the new product
$user->decrement('credits', $newProduct->price);
return redirect()->route('servers.show', ['server' => $server->id])->with('success', __('Server Successfully Upgraded'));
} else {

View file

@ -3,36 +3,31 @@
namespace App\Http\Controllers;
use App\Models\ShopProduct;
use App\Settings\GeneralSettings;
use App\Settings\UserSettings;
use Illuminate\Support\Facades\Auth;
class StoreController extends Controller
{
/** Display a listing of the resource. */
public function index()
public function index(UserSettings $user_settings, GeneralSettings $general_settings)
{
$isPaymentSetup = false;
if (
env('APP_ENV') == 'local' ||
config('SETTINGS::PAYMENTS:PAYPAL:SECRET') && config('SETTINGS::PAYMENTS:PAYPAL:CLIENT_ID') ||
config('SETTINGS::PAYMENTS:STRIPE:SECRET') && config('SETTINGS::PAYMENTS:STRIPE:ENDPOINT_SECRET') && config('SETTINGS::PAYMENTS:STRIPE:METHODS')
) {
$isPaymentSetup = true;
}
$isStoreEnabled = $general_settings->store_enabled;
//Required Verification for creating an server
if (config('SETTINGS::USER:FORCE_EMAIL_VERIFICATION', false) === 'true' && ! Auth::user()->hasVerifiedEmail()) {
if ($user_settings->force_email_verification && !Auth::user()->hasVerifiedEmail()) {
return redirect()->route('profile.index')->with('error', __('You are required to verify your email address before you can purchase credits.'));
}
//Required Verification for creating an server
if (config('SETTINGS::USER:FORCE_DISCORD_VERIFICATION', false) === 'true' && ! Auth::user()->discordUser) {
if ($user_settings->force_discord_verification && !Auth::user()->discordUser) {
return redirect()->route('profile.index')->with('error', __('You are required to link your discord account before you can purchase Credits'));
}
return view('store.index')->with([
'products' => ShopProduct::where('disabled', '=', false)->orderBy('type', 'asc')->orderBy('price', 'asc')->get(),
'isPaymentSetup' => $isPaymentSetup,
'isStoreEnabled' => $isStoreEnabled,
'credits_display_name' => $general_settings->credits_display_name
]);
}
}

View file

@ -11,6 +11,9 @@ use App\Models\User;
use App\Notifications\Ticket\Admin\AdminCreateNotification;
use App\Notifications\Ticket\Admin\AdminReplyNotification;
use App\Notifications\Ticket\User\CreateNotification;
use App\Settings\LocaleSettings;
use App\Settings\PterodactylSettings;
use App\Settings\TicketSettings;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Notification;
@ -18,23 +21,32 @@ use Illuminate\Support\Str;
class TicketsController extends Controller
{
public function index()
const READ_PERMISSION = 'user.ticket.read';
const WRITE_PERMISSION = 'user.ticket.write';
public function index(LocaleSettings $locale_settings, TicketSettings $ticketSettings)
{
$tickets = Ticket::where('user_id', Auth::user()->id)->paginate(10);
$ticketcategories = TicketCategory::all();
return view('ticket.index', compact('tickets', 'ticketcategories'));
return view('ticket.index', [
'ticketsettings' => $ticketSettings,
'tickets' => Ticket::where('user_id', Auth::user()->id)->paginate(10),
'ticketcategories' => TicketCategory::all(),
'locale_datatables' => $locale_settings->datatables
]);
}
public function store(Request $request)
public function store(Request $request, TicketSettings $ticket_settings)
{
$this->validate($request, [
$this->validate(
$request,
[
'title' => 'required',
'ticketcategory' => 'required',
'priority' => 'required',
'message' => 'required',]
'message' => 'required',
'g-recaptcha-response' => ['required', 'recaptcha'],
]
);
$ticket = new Ticket([
$ticket = new Ticket(
[
'title' => $request->input('title'),
'user_id' => Auth::user()->id,
'ticket_id' => strtoupper(Str::random(8)),
@ -42,29 +54,26 @@ class TicketsController extends Controller
'priority' => $request->input('priority'),
'message' => $request->input('message'),
'status' => 'Open',
'server' => $request->input('server'),]
'server' => $request->input('server'),
]
);
$ticket->save();
$user = Auth::user();
if (config('SETTINGS::TICKET:NOTIFY') == "all") {
$admin = User::where('role', 'admin')->orWhere('role', 'mod')->get();
}
if (config('SETTINGS::TICKET:NOTIFY') == "admin") {
$admin = User::where('role', 'admin')->get();
}
if (config('SETTINGS::TICKET:NOTIFY') == "moderator") {
$admin = User::where('role', 'mod')->get();
$staffNotify = User::permission('admin.tickets.get_notification')->get();
foreach($staffNotify as $staff){
Notification::send($staff, new AdminCreateNotification($ticket, $user));
}
$user->notify(new CreateNotification($ticket));
if (config('SETTINGS::TICKET:NOTIFY') != "none") {
Notification::send($admin, new AdminCreateNotification($ticket, $user));
}
return redirect()->route('ticket.index')->with('success', __('A ticket has been opened, ID: #') . $ticket->ticket_id);
}
public function show($ticket_id)
public function show($ticket_id, PterodactylSettings $ptero_settings)
{
$this->checkPermission(self::READ_PERMISSION);
try {
$ticket = Ticket::where('ticket_id', $ticket_id)->firstOrFail();
} catch (Exception $e) {
@ -73,8 +82,9 @@ class TicketsController extends Controller
$ticketcomments = $ticket->ticketcomments;
$ticketcategory = $ticket->ticketcategory;
$server = Server::where('id', $ticket->server)->first();
$pterodactyl_url = $ptero_settings->panel_url;
return view('ticket.show', compact('ticket', 'ticketcategory', 'ticketcomments', 'server'));
return view('ticket.show', compact('ticket', 'ticketcategory', 'ticketcomments', 'server', 'pterodactyl_url'));
}
public function reply(Request $request)
@ -99,15 +109,19 @@ class TicketsController extends Controller
'message' => $request->input('message'),
]);
$user = Auth::user();
$admin = User::where('role', 'admin')->orWhere('role', 'mod')->get();
$newmessage = $request->input('ticketcomment');
Notification::send($admin, new AdminReplyNotification($ticket, $user, $newmessage));
$staffNotify = User::permission('admin.tickets.get_notification')->get();
foreach($staffNotify as $staff){
Notification::send($staff, new AdminReplyNotification($ticket, $user, $newmessage));
}
return redirect()->back()->with('success', __('Your comment has been submitted'));
}
public function create()
{
$this->checkPermission(self::WRITE_PERMISSION);
//check in blacklist
$check = TicketBlacklist::where('user_id', Auth::user()->id)->first();
if ($check && $check->status == 'True') {
@ -170,8 +184,10 @@ class TicketsController extends Controller
return __($tickets->priority);
})
->editColumn('updated_at', function (Ticket $tickets) {
return ['display' => $tickets->updated_at ? $tickets->updated_at->diffForHumans() : '',
'raw' => $tickets->updated_at ? strtotime($tickets->updated_at) : ''];
return [
'display' => $tickets->updated_at ? $tickets->updated_at->diffForHumans() : '',
'raw' => $tickets->updated_at ? strtotime($tickets->updated_at) : ''
];
})
->addColumn('actions', function (Ticket $tickets) {
$statusButtonColor = ($tickets->status == "Closed") ? 'btn-success' : 'btn-warning';

View file

@ -4,7 +4,6 @@ namespace App\Http;
use App\Http\Middleware\ApiAuthToken;
use App\Http\Middleware\CheckSuspended;
use App\Http\Middleware\GlobalNames;
use App\Http\Middleware\isAdmin;
use App\Http\Middleware\isMod;
use App\Http\Middleware\LastSeen;
@ -27,6 +26,7 @@ class Kernel extends HttpKernel
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
@ -43,14 +43,12 @@ class Kernel extends HttpKernel
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
LastSeen::class,
GlobalNames::class,
\App\Http\Middleware\SetLocale::class,
],
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
GlobalNames::class,
],
];
@ -76,5 +74,9 @@ class Kernel extends HttpKernel
'moderator' => isMod::class,
'api.token' => ApiAuthToken::class,
'checkSuspended' => CheckSuspended::class,
'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class,
'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class,
];
}

View file

@ -1,27 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class GlobalNames
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
define('CREDITS_DISPLAY_NAME', config('SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME', 'Credits'));
$unsupported_lang_array = explode(',', config('app.unsupported_locales'));
$unsupported_lang_array = array_map('strtolower', $unsupported_lang_array);
define('UNSUPPORTED_LANGS', $unsupported_lang_array);
return $next($request);
}
}

View file

@ -2,6 +2,7 @@
namespace App\Http\Middleware;
use App\Settings\LocaleSettings;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
@ -9,6 +10,12 @@ use Illuminate\Support\Facades\Session;
class SetLocale
{
private $locale_settings;
public function __construct(LocaleSettings $locale_settings)
{
$this->locale_settings = $locale_settings;
}
/**
* Handle an incoming request.
*
@ -19,15 +26,15 @@ class SetLocale
public function handle($request, Closure $next)
{
if (Session::has('locale')) {
$locale = Session::get('locale', config('SETTINGS::LOCALE:DEFAULT'));
$locale = Session::get('locale', $this->locale_settings->default);
} else {
if (config('SETTINGS::LOCALE:DYNAMIC') !== 'true') {
$locale = config('SETTINGS::LOCALE:DEFAULT');
if (!$this->locale_settings->dynamic) {
$locale = $this->locale_settings->default;
} else {
$locale = substr($request->server('HTTP_ACCEPT_LANGUAGE'), 0, 2);
if (! in_array($locale, explode(',', config('SETTINGS::LOCALE:AVAILABLE')))) {
$locale = config('SETTINGS::LOCALE:DEFAULT');
if (! in_array($locale, explode(',', $this->locale_settings->available))) {
$locale = $this->locale_settings->default;
}
}
}

View file

@ -1,27 +0,0 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class isAdmin
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if (Auth::user() && Auth::user()->role == 'admin') {
return $next($request);
}
return redirect(RouteServiceProvider::HOME);
}
}

View file

@ -1,27 +0,0 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class isMod
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if (Auth::user() && Auth::user()->role == 'moderator' || Auth::user() && Auth::user()->role == 'admin') {
return $next($request);
}
return redirect(RouteServiceProvider::HOME);
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace App\Listeners;
use App\Events\CouponUsedEvent;
use App\Settings\CouponSettings;
use Carbon\Carbon;
class CouponUsed
{
private $delete_coupon_on_expires;
private $delete_coupon_on_uses_reached;
/**
* Create the event listener.
*
* @return void
*/
public function __construct(CouponSettings $couponSettings)
{
$this->delete_coupon_on_expires = $couponSettings->delete_coupon_on_expires;
$this->delete_coupon_on_uses_reached = $couponSettings->delete_coupon_on_uses_reached;
}
/**
* Handle the event.
*
* @param \App\Events\CouponUsedEvent $event
* @return void
*/
public function handle(CouponUsedEvent $event)
{
// Automatically increments the coupon usage.
$this->incrementUses($event);
if ($this->delete_coupon_on_expires) {
if (!is_null($event->coupon->expired_at)) {
if ($event->coupon->expires_at <= Carbon::now()->timestamp) {
$event->coupon->delete();
}
}
}
if ($this->delete_coupon_on_uses_reached) {
if ($event->coupon->uses >= $event->coupon->max_uses) {
$event->coupon->delete();
}
}
}
/**
* Increments the use of a coupon.
*
* @param \App\Events\CouponUsedEvent $event
*/
private function incrementUses(CouponUsedEvent $event)
{
$event->coupon->increment('uses');
$event->coupon->save();
}
}

View file

@ -3,13 +3,27 @@
namespace App\Listeners;
use App\Events\PaymentEvent;
use App\Settings\InvoiceSettings;
use App\Traits\Invoiceable;
class CreateInvoice
{
use Invoiceable;
private $invoice_enabled;
private $invoice_settings;
/**
* Create the event listener.
*
* @return void
*/
public function __construct(InvoiceSettings $invoice_settings)
{
$this->invoice_enabled = $invoice_settings->enabled;
$this->invoice_settings = $invoice_settings;
}
/**
* Handle the event.
*
@ -18,9 +32,9 @@ class CreateInvoice
*/
public function handle(PaymentEvent $event)
{
if (config('SETTINGS::INVOICE:ENABLED') == 'true') {
if ($this->invoice_enabled) {
// create invoice using the trait
$this->createInvoice($event->payment, $event->shopProduct);
$this->createInvoice($event->payment, $event->shopProduct, $this->invoice_settings);
}
}
}

View file

@ -4,11 +4,24 @@ namespace App\Listeners;
use App\Events\UserUpdateCreditsEvent;
use App\Models\Server;
use App\Settings\UserSettings;
use Exception;
use Illuminate\Contracts\Queue\ShouldQueue;
class UnsuspendServers implements ShouldQueue
{
private $min_credits_to_make_server;
/**
* Create the event listener.
*
* @return void
*/
public function __construct(UserSettings $user_settings)
{
$this->min_credits_to_make_server = $user_settings->min_credits_to_make_server;
}
/**
* Handle the event.
*
@ -19,7 +32,7 @@ class UnsuspendServers implements ShouldQueue
*/
public function handle(UserUpdateCreditsEvent $event)
{
if ($event->user->credits > config('SETTINGS::USER:MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER', 50)) {
if ($event->user->credits > $this->min_credits_to_make_server) {
/** @var Server $server */
foreach ($event->user->servers as $server) {
if ($server->isSuspended()) {

View file

@ -2,15 +2,41 @@
namespace App\Listeners;
use App\Enums\PaymentStatus;
use App\Events\PaymentEvent;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use App\Models\PartnerDiscount;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use App\Settings\GeneralSettings;
use App\Settings\ReferralSettings;
use App\Settings\UserSettings;
class UserPayment
{
private $server_limit_after_irl_purchase;
private $referral_mode;
private $referral_percentage;
private $referral_always_give_commission;
private $credits_display_name;
/**
* Create the event listener.
*
* @return void
*/
public function __construct(UserSettings $user_settings, ReferralSettings $referral_settings, GeneralSettings $general_settings)
{
$this->server_limit_after_irl_purchase = $user_settings->server_limit_after_irl_purchase;
$this->referral_mode = $referral_settings->mode;
$this->referral_percentage = $referral_settings->percentage;
$this->referral_always_give_commission = $referral_settings->always_give_commission;
$this->credits_display_name = $general_settings->credits_display_name;
}
/**
* Handle the event.
*
@ -23,13 +49,13 @@ class UserPayment
$shopProduct = $event->shopProduct;
// only update user if payment is paid
if ($event->payment->status != "paid") {
if ($event->payment->status != PaymentStatus::PAID->value) {
return;
}
//update server limit
if (config('SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE') !== 0 && $user->server_limit < config('SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE')) {
$user->update(['server_limit' => config('SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE')]);
if ($this->server_limit_after_irl_purchase !== 0 && $user->server_limit < $this->server_limit_after_irl_purchase) {
$user->update(['server_limit' => $this->server_limit_after_irl_purchase]);
}
//update User with bought item
@ -40,35 +66,35 @@ class UserPayment
}
//give referral commission always
if ((config("SETTINGS::REFERRAL:MODE") == "commission" || config("SETTINGS::REFERRAL:MODE") == "both") && $shopProduct->type == "Credits" && config("SETTINGS::REFERRAL::ALWAYS_GIVE_COMMISSION") == "true") {
if (($this->referral_mode === "commission" || $this->referral_mode === "both") && $shopProduct->type == "Credits" && $this->referral_always_give_commission) {
if ($ref_user = DB::table("user_referrals")->where('registered_user_id', '=', $user->id)->first()) {
$ref_user = User::findOrFail($ref_user->referral_id);
$increment = number_format($shopProduct->quantity * (PartnerDiscount::getCommission($ref_user->id)) / 100, 0, "", "");
$increment = number_format($shopProduct->quantity * (PartnerDiscount::getCommission($ref_user->id, $this->referral_percentage)) / 100, 0, "", "");
$ref_user->increment('credits', $increment);
//LOGS REFERRALS IN THE ACTIVITY LOG
activity()
->performedOn($user)
->causedBy($ref_user)
->log('gained ' . $increment . ' ' . config("SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME") . ' for commission-referral of ' . $user->name . ' (ID:' . $user->id . ')');
->log('gained ' . $increment . ' ' . $this->credits_display_name . ' for commission-referral of ' . $user->name . ' (ID:' . $user->id . ')');
}
}
//update role give Referral-reward
if ($user->role == 'member') {
$user->update(['role' => 'client']);
if ($user->hasRole(4)) {
$user->syncRoles(3);
//give referral commission only on first purchase
if ((config("SETTINGS::REFERRAL:MODE") == "commission" || config("SETTINGS::REFERRAL:MODE") == "both") && $shopProduct->type == "Credits" && config("SETTINGS::REFERRAL::ALWAYS_GIVE_COMMISSION") == "false") {
if (($this->referral_mode === "commission" || $this->referral_mode === "both") && $shopProduct->type == "Credits" && !$this->referral_always_give_commission) {
if ($ref_user = DB::table("user_referrals")->where('registered_user_id', '=', $user->id)->first()) {
$ref_user = User::findOrFail($ref_user->referral_id);
$increment = number_format($shopProduct->quantity * (PartnerDiscount::getCommission($ref_user->id)) / 100, 0, "", "");
$increment = number_format($shopProduct->quantity * (PartnerDiscount::getCommission($ref_user->id, $this->referral_percentage)) / 100, 0, "", "");
$ref_user->increment('credits', $increment);
//LOGS REFERRALS IN THE ACTIVITY LOG
activity()
->performedOn($user)
->causedBy($ref_user)
->log('gained ' . $increment . ' ' . config("SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME") . ' for commission-referral of ' . $user->name . ' (ID:' . $user->id . ')');
->log('gained ' . $increment . ' ' . $this->credits_display_name . ' for commission-referral of ' . $user->name . ' (ID:' . $user->id . ')');
}
}
}

View file

@ -2,16 +2,22 @@
namespace App\Listeners;
use App\Settings\UserSettings;
class Verified
{
private $server_limit_after_verify_email;
private $credits_reward_after_verify_email;
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
public function __construct(UserSettings $user_settings)
{
//
$this->server_limit_after_verify_email = $user_settings->server_limit_after_verify_email;
$this->credits_reward_after_verify_email = $user_settings->credits_reward_after_verify_email;
}
/**
@ -23,8 +29,8 @@ class Verified
public function handle($event)
{
if (!$event->user->email_verified_reward) {
$event->user->increment('server_limit', config('SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL'));
$event->user->increment('credits', config('SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_EMAIL'));
$event->user->increment('server_limit', $this->server_limit_after_verify_email);
$event->user->increment('credits', $this->credits_reward_after_verify_email);
$event->user->update(['email_verified_reward' => true]);
}
}

119
app/Models/Coupon.php Normal file
View file

@ -0,0 +1,119 @@
<?php
namespace App\Models;
use App\Settings\CouponSettings;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Coupon extends Model
{
use HasFactory, LogsActivity;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnlyDirty()
->logOnly(['*'])
->dontSubmitEmptyLogs();
}
/**
* @var string[]
*/
protected $fillable = [
'code',
'type',
'value',
'uses',
'max_uses',
'expires_at'
];
/**
* @var string[]
*/
protected $casts = [
'value' => 'float',
'uses' => 'integer',
'max_uses' => 'integer',
'expires_at' => 'timestamp'
];
/**
* Returns the date format used by the coupons.
*
* @return string
*/
public static function formatDate(): string
{
return 'Y-MM-DD HH:mm:ss';
}
/**
* Returns the current state of the coupon.
*
* @return string
*/
public function getStatus()
{
if ($this->uses >= $this->max_uses) {
return 'USES_LIMIT_REACHED';
}
if (!is_null($this->expires_at)) {
if ($this->expires_at <= Carbon::now(config('app.timezone'))->timestamp) {
return __('EXPIRED');
}
}
return __('VALID');
}
/**
* Check if a user has already exceeded the uses of a coupon.
*
* @param User $user The request being made.
*
* @return bool
*/
public function isMaxUsesReached($user): bool
{
$coupon_settings = new CouponSettings;
$coupon_uses = $user->coupons()->where('id', $this->id)->count();
return $coupon_uses >= $coupon_settings->max_uses_per_user;
}
/**
* Generate a specified quantity of coupon codes.
*
* @param int $amount Amount of coupons to be generated.
*
* @return array
*/
public static function generateRandomCoupon(int $amount = 10): array
{
$coupons = [];
for ($i = 0; $i < $amount; $i++) {
$random_coupon = strtoupper(bin2hex(random_bytes(3)));
$coupons[] = $random_coupon;
}
return $coupons;
}
/**
* @return BelongsToMany
*/
public function users()
{
return $this->belongsToMany(User::class, 'user_coupons');
}
}

View file

@ -33,7 +33,7 @@ class PartnerDiscount extends Model
return 0;
}
public static function getCommission($user_id)
public static function getCommission($user_id, $percentage)
{
if ($partnerDiscount = PartnerDiscount::where('user_id', $user_id)->first()) {
if ($partnerDiscount->referral_system_commission >= 0) {
@ -41,6 +41,6 @@ class PartnerDiscount extends Model
}
}
return config('SETTINGS::REFERRAL:PERCENTAGE');
return $percentage;
}
}

View file

@ -9,6 +9,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
use App\Models\Pterodactyl\Egg;
use App\Models\Pterodactyl\Node;
class Product extends Model
{
@ -43,12 +45,46 @@ class Product extends Model
public function getHourlyPrice()
{
return ($this->price / 30) / 24;
// calculate the hourly price with the billing period
switch($this->billing_period) {
case 'daily':
return $this->price / 24;
case 'weekly':
return $this->price / 24 / 7;
case 'monthly':
return $this->price / 24 / 30;
case 'quarterly':
return $this->price / 24 / 30 / 3;
case 'half-annually':
return $this->price / 24 / 30 / 6;
case 'annually':
return $this->price / 24 / 365;
default:
return $this->price;
}
}
public function getDailyPrice()
public function getMonthlyPrice()
{
return $this->price / 30;
// calculate the hourly price with the billing period
switch($this->billing_period) {
case 'hourly':
return $this->price * 24 * 30;
case 'daily':
return $this->price * 30;
case 'weekly':
return $this->price * 4;
case 'monthly':
return $this->price;
case 'quarterly':
return $this->price / 3;
case 'half-annually':
return $this->price / 6;
case 'annually':
return $this->price / 12;
default:
return $this->price;
}
}
public function getWeeklyPrice()

View file

@ -1,12 +1,14 @@
<?php
namespace App\Models;
namespace App\Models\Pterodactyl;
use App\Classes\Pterodactyl;
use App\Classes\PterodactylClient;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use App\Models\Pterodactyl\Nest;
use App\Models\Product;
class Egg extends Model
{
@ -37,9 +39,9 @@ class Egg extends Model
public static function syncEggs()
{
Nest::syncNests();
Nest::all()->each(function (Nest $nest) {
$eggs = Pterodactyl::getEggs($nest);
$client = app(PterodactylClient::class);
Nest::all()->each(function (Nest $nest) use ($client) {
$eggs = $client->getEggs($nest);
foreach ($eggs as $egg) {
$array = [];

View file

@ -1,8 +1,8 @@
<?php
namespace App\Models;
namespace App\Models\Pterodactyl;
use App\Classes\Pterodactyl;
use App\Classes\PterodactylClient;
use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -33,7 +33,8 @@ class Location extends Model
*/
public static function syncLocations()
{
$locations = Pterodactyl::getLocations();
$client = app(PterodactylClient::class);
$locations = $client->getLocations();
//map response
$locations = array_map(function ($val) {

View file

@ -1,8 +1,8 @@
<?php
namespace App\Models;
namespace App\Models\Pterodactyl;
use App\Classes\Pterodactyl;
use App\Classes\PterodactylClient;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -32,7 +32,8 @@ class Nest extends Model
public static function syncNests()
{
$nests = Pterodactyl::getNests();
$client = app(PterodactylClient::class);
$nests = $client->getNests();
//map response
$nests = array_map(function ($nest) {

View file

@ -1,13 +1,14 @@
<?php
namespace App\Models;
namespace App\Models\Pterodactyl;
use App\Classes\Pterodactyl;
use App\Classes\PterodactylClient;
use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use App\Models\Product;
class Node extends Model
{
@ -32,7 +33,8 @@ class Node extends Model
public static function syncNodes()
{
Location::syncLocations();
$nodes = Pterodactyl::getNodes();
$client = app(PterodactylClient::class);
$nodes = $client->getNodes();
//map response
$nodes = array_map(function ($node) {

View file

@ -2,7 +2,9 @@
namespace App\Models;
use App\Classes\Pterodactyl;
use Carbon\Carbon;
use App\Classes\PterodactylClient;
use App\Settings\PterodactylSettings;
use Exception;
use GuzzleHttp\Promise\PromiseInterface;
use Hidehalo\Nanoid\Client;
@ -21,13 +23,17 @@ class Server extends Model
{
use HasFactory;
use LogsActivity;
private PterodactylClient $pterodactyl;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
-> logOnlyDirty()
-> logOnly(['*'])
-> dontSubmitEmptyLogs();
->logOnlyDirty()
->logOnly(['*'])
->dontSubmitEmptyLogs();
}
/**
* @var bool
*/
@ -47,12 +53,14 @@ class Server extends Model
* @var string[]
*/
protected $fillable = [
'name',
'description',
'suspended',
'identifier',
'product_id',
'pterodactyl_id',
"name",
"description",
"suspended",
"identifier",
"product_id",
"pterodactyl_id",
"last_billed",
"canceled"
];
/**
@ -62,6 +70,14 @@ class Server extends Model
'suspended' => 'datetime',
];
public function __construct()
{
parent::__construct();
$ptero_settings = new PterodactylSettings();
$this->pterodactyl = new PterodactylClient($ptero_settings);
}
public static function boot()
{
parent::boot();
@ -73,8 +89,8 @@ class Server extends Model
});
static::deleting(function (Server $server) {
$response = Pterodactyl::client()->delete("/application/servers/{$server->pterodactyl_id}");
if ($response->failed() && ! is_null($server->pterodactyl_id)) {
$response = $server->pterodactyl->application->delete("/application/servers/{$server->pterodactyl_id}");
if ($response->failed() && !is_null($server->pterodactyl_id)) {
//only return error when it's not a 404 error
if ($response['errors'][0]['status'] != '404') {
throw new Exception($response['errors'][0]['code']);
@ -88,7 +104,7 @@ class Server extends Model
*/
public function isSuspended()
{
return ! is_null($this->suspended);
return !is_null($this->suspended);
}
/**
@ -96,7 +112,7 @@ class Server extends Model
*/
public function getPterodactylServer()
{
return Pterodactyl::client()->get("/application/servers/{$this->pterodactyl_id}");
return $this->pterodactyl->application->get("/application/servers/{$this->pterodactyl_id}");
}
/**
@ -104,7 +120,7 @@ class Server extends Model
*/
public function suspend()
{
$response = Pterodactyl::suspendServer($this);
$response = $this->pterodactyl->suspendServer($this);
if ($response->successful()) {
$this->update([
@ -120,14 +136,16 @@ class Server extends Model
*/
public function unSuspend()
{
$response = Pterodactyl::unSuspendServer($this);
$response = $this->pterodactyl->unSuspendServer($this);
if ($response->successful()) {
$this->update([
'suspended' => null,
'last_billed' => Carbon::now()->toDateTimeString(),
]);
}
return $this;
}

View file

@ -4,48 +4,8 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class Settings extends Model
{
use HasFactory;
protected $table = 'settings';
public const CACHE_TAG = 'setting';
public $primaryKey = 'key';
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = [
'key',
'value',
'type',
];
public static function boot()
{
parent::boot();
static::updated(function (Settings $settings) {
Cache::forget(self::CACHE_TAG.':'.$settings->key);
});
}
/**
* @param string $key
* @param $default
* @return mixed
*/
public static function getValueByKey(string $key, $default = null)
{
return Cache::rememberForever(self::CACHE_TAG.':'.$key, function () use ($default, $key) {
$settings = self::find($key);
return $settings ? $settings->value : $default;
});
}
}

View file

@ -36,6 +36,13 @@ class ShopProduct extends Model
'disabled',
];
/**
* @var string[]
*/
protected $casts = [
'price' => 'float'
];
public static function boot()
{
parent::boot();
@ -73,7 +80,9 @@ class ShopProduct extends Model
public function getPriceAfterDiscount()
{
return number_format($this->price - ($this->price * PartnerDiscount::getDiscount() / 100), 2);
$discountRate = PartnerDiscount::getDiscount() / 100;
$discountedPrice = $this->price * (1 - $discountRate);
return round($discountedPrice, 2);
}
/**
@ -83,7 +92,8 @@ class ShopProduct extends Model
*/
public function getTaxValue()
{
return number_format($this->getPriceAfterDiscount() * $this->getTaxPercent() / 100, 2);
$taxValue = $this->getPriceAfterDiscount() * $this->getTaxPercent() / 100;
return round($taxValue, 2);
}
/**
@ -93,6 +103,7 @@ class ShopProduct extends Model
*/
public function getTotalPrice()
{
return number_format($this->getPriceAfterDiscount() + $this->getTaxValue(), 2);
$total = $this->getPriceAfterDiscount() + $this->getTaxValue();
return round($total, 2);
}
}

View file

@ -2,9 +2,10 @@
namespace App\Models;
use App\Classes\Pterodactyl;
use App\Notifications\Auth\QueuedVerifyEmail;
use App\Notifications\WelcomeMessage;
use App\Classes\PterodactylClient;
use App\Settings\PterodactylSettings;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -12,16 +13,20 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\DB;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\CausesActivity;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Permission\Traits\HasRoles;
/**
* Class User
*/
class User extends Authenticatable implements MustVerifyEmail
{
use HasFactory, Notifiable, LogsActivity, CausesActivity;
use HasFactory, Notifiable, LogsActivity, CausesActivity, HasRoles;
private PterodactylClient $pterodactyl;
/**
* @var string[]
@ -61,7 +66,7 @@ class User extends Authenticatable implements MustVerifyEmail
'avatar',
'suspended',
'referral_code',
'email_verified_reward'
'email_verified_reward',
];
/**
@ -87,6 +92,14 @@ class User extends Authenticatable implements MustVerifyEmail
'email_verified_reward' => 'boolean'
];
public function __construct()
{
parent::__construct();
$ptero_settings = new PterodactylSettings();
$this->pterodactyl = new PterodactylClient($ptero_settings);
}
public static function boot()
{
parent::boot();
@ -113,7 +126,7 @@ class User extends Authenticatable implements MustVerifyEmail
$user->discordUser()->delete();
Pterodactyl::client()->delete("/application/users/{$user->pterodactyl_id}");
$user->pterodactyl->application->delete("/application/users/{$user->pterodactyl_id}");
});
}
@ -157,6 +170,14 @@ class User extends Authenticatable implements MustVerifyEmail
return $this->belongsToMany(Voucher::class);
}
/**
* @return BelongsToMany
*/
public function coupons()
{
return $this->belongsToMany(Coupon::class, 'user_coupons');
}
/**
* @return HasOne
*/
@ -186,9 +207,6 @@ class User extends Authenticatable implements MustVerifyEmail
return $this->suspended;
}
/**
* @throws Exception
*/
public function suspend()
{
foreach ($this->servers as $server) {
@ -202,9 +220,6 @@ class User extends Authenticatable implements MustVerifyEmail
return $this;
}
/**
* @throws Exception
*/
public function unSuspend()
{
foreach ($this->getServersWithProduct() as $server) {
@ -220,45 +235,34 @@ class User extends Authenticatable implements MustVerifyEmail
return $this;
}
private function getServersWithProduct()
{
return $this->servers()
->with('product')
->get();
}
/**
* @return string
*/
public function getAvatar()
{
//TODO loading the images to confirm they exist is causing to much load time. alternative has to be found :) maybe onerror tag on the <img tags>
// if ($this->discordUser()->exists()) {
// if(@getimagesize($this->discordUser->getAvatar())) {
// $avatar = $this->discordUser->getAvatar();
// } else {
// $avatar = "https://www.gravatar.com/avatar/" . md5(strtolower(trim($this->email)));
// }
// } else {
// $avatar = "https://www.gravatar.com/avatar/" . md5(strtolower(trim($this->email)));
// }
return 'https://www.gravatar.com/avatar/' . md5(strtolower(trim($this->email)));
}
/**
* @return string
*/
public function creditUsage()
{
$usage = 0;
foreach ($this->getServersWithProduct() as $server) {
$usage += $server->product->price;
$usage += $server->product->getHourlyPrice() * 24 * 30;
}
return number_format($usage, 2, '.', '');
}
private function getServersWithProduct()
{
return $this->servers()
->whereNull('suspended')
->whereNull('canceled')
->with('product')
->get();
}
/**
* @return array|string|string[]
*/
@ -279,7 +283,7 @@ class User extends Authenticatable implements MustVerifyEmail
public function verifyEmail()
{
$this->forceFill([
'email_verified_at' => now(),
'email_verified_at' => now()
])->save();
}
@ -290,6 +294,17 @@ class User extends Authenticatable implements MustVerifyEmail
])->save();
}
public function referredBy()
{
$referee = DB::table('user_referrals')->where("registered_user_id", $this->id)->first();
if ($referee) {
$referee = User::where("id", $referee->referral_id)->firstOrFail();
return $referee;
}
return Null;
}
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()

View file

@ -3,6 +3,8 @@
namespace App\Notifications;
use App\Models\User;
use App\Settings\GeneralSettings;
use App\Settings\ReferralSettings;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
@ -15,6 +17,12 @@ class ReferralNotification extends Notification
*/
private $user;
private $ref_user;
private $reward;
private $credits_display_name;
/**
* Create a new notification instance.
*
@ -22,6 +30,11 @@ class ReferralNotification extends Notification
*/
public function __construct(int $user, int $ref_user)
{
$general_settings= new GeneralSettings();
$referral_settings = new ReferralSettings();
$this->credits_display_name = $general_settings->credits_display_name;
$this->reward = $referral_settings->reward;
$this->user = User::findOrFail($user);
$this->ref_user = User::findOrFail($ref_user);
}
@ -48,8 +61,8 @@ class ReferralNotification extends Notification
return [
'title' => __('Someone registered using your Code!'),
'content' => '
<p>You received '.config('SETTINGS::REFERRAL::REWARD').' '.config('SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME').'</p>
<p>because '.$this->ref_user->name.' registered with your Referral-Code!</p>
<p>You received '. $this->reward . ' ' . $this->credits_display_name . '</p>
<p>because ' . $this->ref_user->name . ' registered with your Referral-Code!</p>
<p>Thank you very much for supporting us!.</p>
<p>'.config('app.name', 'Laravel').'</p>
',

View file

@ -3,6 +3,8 @@
namespace App\Notifications;
use App\Models\User;
use App\Settings\GeneralSettings;
use App\Settings\UserSettings;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
@ -16,6 +18,16 @@ class WelcomeMessage extends Notification implements ShouldQueue
*/
private $user;
private $credits_display_name;
private $credits_reward_after_verify_discord;
private $credits_reward_after_verify_email;
private $server_limit_after_verify_discord;
private $server_limit_after_verify_email;
/**
* Create a new notification instance.
*
@ -23,7 +35,15 @@ class WelcomeMessage extends Notification implements ShouldQueue
*/
public function __construct(User $user)
{
$general_settings= new GeneralSettings();
$user_settings = new UserSettings();
$this->user = $user;
$this->credits_display_name = $general_settings->credits_display_name;
$this->credits_reward_after_verify_discord = $user_settings->credits_reward_after_verify_discord;
$this->credits_reward_after_verify_email = $user_settings->credits_reward_after_verify_email;
$this->server_limit_after_verify_discord = $user_settings->server_limit_after_verify_discord;
$this->server_limit_after_verify_email = $user_settings->server_limit_after_verify_email;
}
/**
@ -40,18 +60,18 @@ class WelcomeMessage extends Notification implements ShouldQueue
public function AdditionalLines()
{
$AdditionalLine = '';
if (config('SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_EMAIL') != 0) {
$AdditionalLine .= __('Verifying your e-mail address will grant you ').config('SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_EMAIL').' '.__('additional').' '.config('SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME').'. <br />';
if ($this->credits_reward_after_verify_email != 0) {
$AdditionalLine .= __('Verifying your e-mail address will grant you ').$this->credits_reward_after_verify_email.' '.__('additional').' '.$this->credits_display_name.'. <br />';
}
if (config('SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL') != 0) {
$AdditionalLine .= __('Verifying your e-mail will also increase your Server Limit by ').config('SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL').'. <br />';
if ($this->server_limit_after_verify_email != 0) {
$AdditionalLine .= __('Verifying your e-mail will also increase your Server Limit by ').$this->server_limit_after_verify_email.'. <br />';
}
$AdditionalLine .= '<br />';
if (config('SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_DISCORD') != 0) {
$AdditionalLine .= __('You can also verify your discord account to get another ').config('SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_DISCORD').' '.config('SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME').'. <br />';
if ($this->credits_reward_after_verify_discord != 0) {
$AdditionalLine .= __('You can also verify your discord account to get another ').$this->credits_reward_after_verify_discord.' '.$this->credits_display_name.'. <br />';
}
if (config('SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD') != 0) {
$AdditionalLine .= __('Verifying your Discord account will also increase your Server Limit by ').config('SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD').'. <br />';
if ($this->server_limit_after_verify_discord != 0) {
$AdditionalLine .= __('Verifying your Discord account will also increase your Server Limit by ').$this->server_limit_after_verify_discord.'. <br />';
}
return $AdditionalLine;

View file

@ -2,17 +2,19 @@
namespace App\Providers;
use App\Models\Settings;
use App\Models\UsefulLink;
use App\Settings\GeneralSettings;
use App\Settings\MailSettings;
use Exception;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
use Qirolab\Theme\Theme;
class AppServiceProvider extends ServiceProvider
{
/**
@ -54,6 +56,29 @@ class AppServiceProvider extends ServiceProvider
return $ok;
});
// Force HTTPS if APP_URL is set to https
if (config('app.url') && parse_url(config('app.url'), PHP_URL_SCHEME) === 'https') {
URL::forceScheme('https');
}
//get the Github Branch the panel is running on
try {
$stringfromfile = file(base_path() . '/.git/HEAD');
$firstLine = $stringfromfile[0]; //get the string from the array
$explodedstring = explode('/', $firstLine, 3); //seperate out by the "/" in the string
$branchname = $explodedstring[2]; //get the one that is always the branch name
} catch (Exception $e) {
$branchname = 'unknown';
Log::notice($e);
}
config(['BRANCHNAME' => $branchname]);
// Do not run this code if no APP_KEY is set
if (config('app.key') == null) return;
try {
if (Schema::hasColumn('useful_links', 'position')) {
$useful_links = UsefulLink::where("position", "like", "%topbar%")->get()->sortby("id");
@ -63,92 +88,23 @@ class AppServiceProvider extends ServiceProvider
Log::error("Couldnt find useful_links. Probably the installation is not completet. " . $e);
}
//only run if the installer has been executed
try {
$settings = Settings::all();
// Set all configs from database
foreach ($settings as $setting) {
config([$setting->key => $setting->value]);
$generalSettings = $this->app->make(GeneralSettings::class);
if (!file_exists(base_path('themes') . "/" . $generalSettings->theme)) {
$generalSettings->theme = "default";
}
if (!file_exists(base_path('themes') . "/" . config("SETTINGS::SYSTEM:THEME"))) {
config(['SETTINGS::SYSTEM:THEME' => "default"]);
}
if (config('SETTINGS::SYSTEM:THEME') && config('SETTINGS::SYSTEM:THEME') !== config('theme.active')) {
Theme::set(config("SETTINGS::SYSTEM:THEME", "default"), "default");
if ($generalSettings->theme && $generalSettings->theme !== config('theme.active')) {
Theme::set($generalSettings->theme, "default");
} else {
Theme::set("default", "default");
}
// Set Mail Config
//only update config if mail settings have changed in DB
if (
config('mail.default') != config('SETTINGS:MAIL:MAILER') ||
config('mail.mailers.smtp.host') != config('SETTINGS:MAIL:HOST') ||
config('mail.mailers.smtp.port') != config('SETTINGS:MAIL:PORT') ||
config('mail.mailers.smtp.username') != config('SETTINGS:MAIL:USERNAME') ||
config('mail.mailers.smtp.password') != config('SETTINGS:MAIL:PASSWORD') ||
config('mail.mailers.smtp.encryption') != config('SETTINGS:MAIL:ENCRYPTION') ||
config('mail.from.address') != config('SETTINGS:MAIL:FROM_ADDRESS') ||
config('mail.from.name') != config('SETTINGS:MAIL:FROM_NAME')
) {
config(['mail.default' => config('SETTINGS::MAIL:MAILER')]);
config(['mail.mailers.smtp' => [
'transport' => 'smtp',
'host' => config('SETTINGS::MAIL:HOST'),
'port' => config('SETTINGS::MAIL:PORT'),
'encryption' => config('SETTINGS::MAIL:ENCRYPTION'),
'username' => config('SETTINGS::MAIL:USERNAME'),
'password' => config('SETTINGS::MAIL:PASSWORD'),
'timeout' => null,
'auth_mode' => null,
]]);
config(['mail.from' => ['address' => config('SETTINGS::MAIL:FROM_ADDRESS'), 'name' => config('SETTINGS::MAIL:FROM_NAME')]]);
Artisan::call('queue:restart');
}
// Set Recaptcha API Config
// Load recaptcha package if recaptcha is enabled
if (config('SETTINGS::RECAPTCHA:ENABLED') == 'true') {
$this->app->register(\Biscolab\ReCaptcha\ReCaptchaServiceProvider::class);
}
//only update config if recaptcha settings have changed in DB
if (
config('recaptcha.api_site_key') != config('SETTINGS::RECAPTCHA:SITE_KEY') ||
config('recaptcha.api_secret_key') != config('SETTINGS::RECAPTCHA:SECRET_KEY')
) {
config(['recaptcha.api_site_key' => config('SETTINGS::RECAPTCHA:SITE_KEY')]);
config(['recaptcha.api_secret_key' => config('SETTINGS::RECAPTCHA:SECRET_KEY')]);
Artisan::call('config:clear');
Artisan::call('cache:clear');
}
try {
$stringfromfile = file(base_path() . '/.git/HEAD');
$firstLine = $stringfromfile[0]; //get the string from the array
$explodedstring = explode('/', $firstLine, 3); //seperate out by the "/" in the string
$branchname = $explodedstring[2]; //get the one that is always the branch name
} catch (Exception $e) {
$branchname = 'unknown';
Log::notice($e);
}
config(['BRANCHNAME' => $branchname]);
// Set Discord-API Config
config(['services.discord.client_id' => config('SETTINGS::DISCORD:CLIENT_ID')]);
config(['services.discord.client_secret' => config('SETTINGS::DISCORD:CLIENT_SECRET')]);
$settings = $this->app->make(MailSettings::class);
$settings->setConfig();
} catch (Exception $e) {
error_log('Settings Error: Could not load settings from database. The Installation probably is not done yet.');
error_log($e);
Log::error('Settings Error: Could not load settings from database. The Installation probably is not done yet.');
Log::error($e);
Log::error("Couldnt load Settings. Probably the installation is not completet. " . $e);
}
}
}

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