Merge pull request from ControlPanel-gg/development

Development
This commit is contained in:
AVMG 2021-06-29 22:56:52 +02:00 committed by GitHub
commit cc2328de02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 801 additions and 438 deletions

View file

@ -36,8 +36,8 @@ PHPMYADMIN_URL=https://mysql.bitsec.dev
DISCORD_INVITE_URL=https://discord.gg/vrUYdxG4wZ
#GOOGLE RECAPTCHA
RECAPTCHA_SITE_KEY=YOUR_API_SITE_KEY
RECAPTCHA_SECRET_KEY=YOUR_API_SECRET_KEY
RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
RECAPTCHA_SECRET_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
MAIL_MAILER=smtp
MAIL_HOST=mailhog

3
.gitignore vendored
View file

@ -5,8 +5,9 @@
/vendor
/storage/credit_deduction_log
.env
.idea
.env.testing
.env.backup
.idea
.phpunit.result.cache
docker-compose.override.yml
Homestead.json

211
README.md
View file

@ -1,192 +1,25 @@
# ControlPanel's Dashboard
### Features
- PayPal Integration
- Email Verification
- Audit Log
- Admin Dashboard
- User/Server Management
- Store (credit system)
- and so much more!
# ControlPanel-gg
![controlpanel](https://user-images.githubusercontent.com/45005889/123518824-06b05000-d6a8-11eb-91b9-d1ed36bd2317.png)
![](https://img.shields.io/github/stars/ControlPanel-gg/dashboard) ![](https://img.shields.io/github/forks/ControlPanel-gg/dashboard) ![](https://img.shields.io/github/tag/ControlPanel-gg/dashboard) ![](https://img.shields.io/github/issues/ControlPanel-gg/dashboard) ![](https://img.shields.io/github/license/ControlPanel-gg/dashboard) ![](https://img.shields.io/discord/787829714483019826)
## About
ControlPanel'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.
This dashboard is built with Laravel as it offers a very robust and secure development environment.
This dashboard offers an easy to use and free billing solution for all starting and experienced hosting providers. This dashboard has many customization options and added discord 0auth verification to offer a solid link between your discord server and your dashboard.
I copied a part of pterodactyls documentation for the installation because it uses the same dependencies and covers this area pretty good.
## Dependencies
* PHP `7.4` or `8.0` (recommended) with the following extensions: `cli`, `openssl`, `gd`, `mysql`, `PDO`, `mbstring`, `tokenizer`, `bcmath`, `xml` or `dom`, `curl`, `zip`, and `fpm` if you are planning to use NGINX.
* MySQL `5.7.22` or higher (MySQL `8` recommended) **or** MariaDB `10.2` or higher.
* Redis (`redis-server`)
* A webserver (Apache, NGINX, Caddy, etc.)
* `curl`
* `tar`
* `unzip`
* `git`
* `composer` v2
### Example Dependency Installation
if you already have pterodactyl installed you can skip this step!
The commands below are simply an example of how you might install these dependencies. Please consult with your
operating system's package manager to determine the correct packages to install.
``` bash
# Add "add-apt-repository" command
apt -y install software-properties-common curl apt-transport-https ca-certificates gnupg
# Add additional repositories for PHP, Redis, and MariaDB
LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php
add-apt-repository -y ppa:chris-lea/redis-server
curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash
# Update repositories list
apt update
# Add universe repository if you are on Ubuntu 18.04
apt-add-repository universe
# Install Dependencies
apt -y install php8.0 php8.0-{cli,gd,mysql,pdo,mbstring,tokenizer,bcmath,xml,fpm,curl,zip} mariadb-server nginx tar unzip git redis-server
```
### Extra dependency used on this dashboard
you need to install this, use the appropriate php version (php -v)
```bash
sudo apt-get install php8.0-intl
```
### Installing Composer
if you already have pterodactyl installed you can skip this step!
Composer is a dependency manager for PHP that allows us to ship everything you'll need code wise to operate the Panel. You'll
need composer installed before continuing in this process.
``` bash
curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer
```
## Download Files
The first step in this process is to create the folder where the panel will live and then move ourselves into that
newly created folder. Below is an example of how to perform this operation.
``` bash
mkdir -p /var/www/dashboard
cd /var/www/dashboard
```
``` bash
git clone https://github.com/ControlPanel-gg/dashboard.git ./
chmod -R 755 storage/* bootstrap/cache/
```
## Installation
Now that all of the files have been downloaded we need to configure some core aspects of the Panel.
You will need a database setup and a user with the correct permissions created for that database before
continuing any further.
First we will copy over our default environment settings file, install core dependencies, and then generate a
new application encryption key.
``` bash
cp .env.example .env
composer install
# Only run the command below if you are installing this Panel
php artisan key:generate --force
# you should create a symbolic link from public/storage to storage/app/public
php artisan storage:link
```
Back up your encryption key (APP_KEY in the `.env` file). It is used as an encryption key for all data that needs to be stored securely (e.g. api keys).
Store it somewhere safe - not just on your server. If you lose it all encrypted data is irrecoverable -- even if you have database backups.
### Environment Configuration
Simply edit the .env to your needs
Please **do not** forget to enter the database creds in here, or the next step won't work
Please **do not** forget to enter your pterodactyl api key in here, or the next steps won't work
``` bash
nano .env
```
### Database Setup
Now we need to setup all of the base data for the Panel in the database you created earlier. **The command below
may take some time to run depending on your machine. Please _DO NOT_ exit the process until it is completed!** This
command will setup the database tables and then add all of the Nests & Eggs that power Pterodactyl.
``` bash
php artisan migrate --seed --force
```
### Add some example products
This step is optional, only run this once
``` bash
php artisan db:seed --class=ExampleItemsSeeder --force
```
### Add The First User
``` bash
php artisan make:user
```
### Set Permissions
The last step in the installation process is to set the correct permissions on the Panel files so that the webserver can
use them correctly.
``` bash
# If using NGINX or Apache (not on CentOS):
chown -R www-data:www-data /var/www/dashboard/*
# If using NGINX on CentOS:
chown -R nginx:nginx /var/www/dashboard/*
# If using Apache on CentOS
chown -R apache:apache /var/www/dashboard/*
```
### Crontab Configuration
The first thing we need to do is create a new cronjob that runs every minute to process specific Dashboard tasks. like billing users hourly and suspending unpaid servers
```bash
* * * * * php /var/www/dashboard/artisan schedule:run >> /dev/null 2>&1
```
### Nginx
You should paste the contents of the file below, replacing <domain> with your domain name being used in a file called dashboard.conf and place it in /etc/nginx/sites-available/, or — if on CentOS, /etc/nginx/conf.d/.
```bash
server {
listen 80;
root /var/www/dashboard/public;
index index.php index.html index.htm index.nginx-debian.html;
server_name YOUR.DOMAIN.COM;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
}
location ~ /\.ht {
deny all;
}
}
```
### Enable configuration
The final step is to enable your NGINX configuration and restart it.
```bash
# You do not need to symlink this file if you are using CentOS.
sudo ln -s /etc/nginx/sites-available/dashboard.conf /etc/nginx/sites-enabled/dashboard.conf
# Check for nginx errors
sudo nginx -t
# You need to restart nginx regardless of OS. only do this you haven't received any errors
systemctl restart nginx
```
### [Installation](https://github.com/ControlPanel-gg/dashboard/wiki "Installation")
### [Updating](https://github.com/ControlPanel-gg/dashboard/wiki/Updating "Updating")
### [Discord](https://discord.gg/4Y6HjD2uyU "discord")
### [Contributing](https://github.com/ControlPanel-gg/dashboard/wiki/Contributing "Contributing")
### [Donating](https://github.com/ControlPanel-gg/dashboard/wiki#donating "Donating")

View file

@ -49,8 +49,12 @@ class Pterodactyl
$response = self::getAllocations($node);
$freeAllocations = [];
foreach ($response['data'] as $allocation) {
if (!$allocation['attributes']['assigned']) array_push($freeAllocations, $allocation);
if(isset($response['data'])){
if (!empty($response['data'])) {
foreach ($response['data'] as $allocation) {
if (!$allocation['attributes']['assigned']) array_push($freeAllocations, $allocation);
}
}
}
return $freeAllocations;

View file

@ -2,9 +2,10 @@
namespace App\Console\Commands;
use App\Classes\Pterodactyl;
use App\Models\Product;
use App\Models\Server;
use Carbon\Carbon;
use App\Models\User;
use App\Notifications\ServersSuspendedNotification;
use Illuminate\Console\Command;
class ChargeCreditsCommand extends Command
@ -23,6 +24,13 @@ class ChargeCreditsCommand extends Command
*/
protected $description = 'Charge all users with active servers';
/**
* A list of users that have to be notified
* @var array
*/
protected $usersToNotify = [];
/**
* Create a new command instance.
*
@ -40,42 +48,54 @@ class ChargeCreditsCommand extends Command
*/
public function handle()
{
Server::chunk(10, function ($servers) {
Server::whereNull('suspended')->chunk(10, function ($servers) {
/** @var Server $server */
foreach ($servers as $server) {
//ignore suspended servers
if ($server->isSuspended()) {
echo Carbon::now()->isoFormat('LLL') . " Ignoring suspended server";
continue;
}
//vars
/** @var Product $product */
$product = $server->product;
/** @var User $user */
$user = $server->user;
$price = ($server->product->price / 30) / 24;
//remove credits or suspend server
if ($user->credits >= $price) {
$user->decrement('credits', $price);
//log
echo Carbon::now()->isoFormat('LLL') . " [CREDIT DEDUCTION] Removed " . number_format($price, 2, '.', '') . " from user (" . $user->name . ") for server (" . $server->name . ")\n";
#charge credits / suspend server
if ($user->credits >= $product->getHourlyPrice()) {
$this->line("<fg=blue>{$user->name}</> Current credits: <fg=green>{$user->credits}</> Credits to be removed: <fg=red>{$product->getHourlyPrice()}</>");
$user->decrement('credits', $product->getHourlyPrice());
} else {
$response = Pterodactyl::client()->post("/application/servers/{$server->pterodactyl_id}/suspend");
try {
#suspend server
$this->line("<fg=yellow>{$server->name}</> from user: <fg=blue>{$user->name}</> has been <fg=red>suspended!</>");
$server->suspend();
if ($response->successful()) {
echo Carbon::now()->isoFormat('LLL') . " [CREDIT DEDUCTION] Suspended server (" . $server->name . ") from user (" . $user->name . ")\n";
$server->update(['suspended' => now()]);
} else {
echo Carbon::now()->isoFormat('LLL') . " [CREDIT DEDUCTION] CRITICAL ERROR! Unable to suspend server (" . $server->name . ") from user (" . $user->name . ")\n";
dump($response->json());
#add user to notify list
if (!in_array($user, $this->usersToNotify)) {
array_push($this->usersToNotify, $user);
}
} catch (\Exception $exception) {
$this->error($exception->getMessage());
}
}
}
}
});
return 'Charged credits for existing servers!\n';
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

@ -26,7 +26,6 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule)
{
$schedule->command('credits:charge')->hourly();
$schedule->command('queue:work --once')->everyMinute();
//log cronjob activity
$schedule->call(function () {

View file

@ -3,11 +3,17 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Configuration;
use App\Models\Payment;
use App\Models\PaypalProduct;
use App\Models\Product;
use App\Models\User;
use App\Notifications\ConfirmPaymentNotification;
use Exception;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -20,15 +26,11 @@ use PayPalHttp\HttpException;
class PaymentController extends Controller
{
protected $allowedAmounts = [
'87',
'350',
'1000',
'2000',
'4000'
];
public function index(){
/**
* @return Application|Factory|View
*/
public function index()
{
return view('admin.payments.index')->with([
'payments' => Payment::paginate(15)
]);
@ -51,15 +53,16 @@ class PaymentController extends Controller
* @param PaypalProduct $paypalProduct
* @return RedirectResponse
*/
public function pay(Request $request , PaypalProduct $paypalProduct)
public function pay(Request $request, PaypalProduct $paypalProduct)
{
$request = new OrdersCreateRequest();
$request->prefer('return=representation');
$request->body = [
"intent" => "CAPTURE",
"purchase_units" => [
"intent" => "CAPTURE",
"purchase_units" => [
[
"reference_id" => uniqid(),
"description" => $paypalProduct->description,
"amount" => [
"value" => $paypalProduct->price,
"currency_code" => strtoupper($paypalProduct->currency_code)
@ -69,7 +72,8 @@ class PaymentController extends Controller
"application_context" => [
"cancel_url" => route('payment.cancel'),
"return_url" => route('payment.success', ['product' => $paypalProduct->id]),
'brand_name' => config('app.name', 'Laravel') ,
'brand_name' => config('app.name', 'Laravel'),
'shipping_preference' => 'NO_SHIPPING'
]
];
@ -120,7 +124,10 @@ class PaymentController extends Controller
*/
public function success(Request $laravelRequest)
{
/** @var PaypalProduct $paypalProduct */
$paypalProduct = PaypalProduct::findOrFail($laravelRequest->input('product'));
/** @var User $user */
$user = Auth::user();
$request = new OrdersCaptureRequest($laravelRequest->input('token'));
$request->prefer('return=representation');
@ -130,32 +137,38 @@ class PaymentController extends Controller
if ($response->statusCode == 201 || $response->statusCode == 200) {
//update credits
Auth::user()->increment('credits', $paypalProduct->quantity);
$user->increment('credits', $paypalProduct->quantity);
//update server limit
if (Auth::user()->server_limit < 10) {
Auth::user()->update(['server_limit' => 10]);
if (Configuration::getValueByKey('SERVER_LIMIT_AFTER_IRL_PURCHASE', 10) !== 0) {
if ($user->server_limit < Configuration::getValueByKey('SERVER_LIMIT_AFTER_IRL_PURCHASE', 10)) {
$user->update(['server_limit' => 10]);
}
}
//update role
if (Auth::user()->role == 'member') {
Auth::user()->update(['role' => 'client']);
if ($user->role == 'member') {
$user->update(['role' => 'client']);
}
//store payment
Payment::create([
'user_id' => Auth::user()->id,
$payment = Payment::create([
'user_id' => $user->id,
'payment_id' => $response->result->id,
'payer_id' => $laravelRequest->input('PayerID'),
'type' => 'Credits',
'status' => $response->result->status,
'amount' => $paypalProduct->quantity,
'price' => $paypalProduct->price,
'currency_code' => $paypalProduct->currency_code,
'payer' => json_encode($response->result->payer),
]);
//payment notification
$user->notify(new ConfirmPaymentNotification($payment));
//redirect back to home
return redirect()->route('home')->with('success', 'Credits have been increased!');
return redirect()->route('home')->with('success', 'Your credit balance has been increased!');
}
// If call returns body in response, you can get the deserialized version from the result attribute of the response
@ -183,6 +196,28 @@ class PaymentController extends Controller
*/
public function cancel(Request $request)
{
return redirect()->route('store.index')->with('success', 'Payment Canceled');
return redirect()->route('store.index')->with('success', 'Payment was Cannceled');
}
/**
* @return JsonResponse|mixed
* @throws Exception
*/
public function dataTable()
{
$query = Payment::with('user');
return datatables($query)
->editColumn('user', function (Payment $payment) {
return $payment->user->name;
})
->editColumn('price', function (Payment $payment) {
return $payment->formatCurrency();
})
->editColumn('created_at', function (Payment $payment) {
return $payment->created_at ? $payment->created_at->diffForHumans() : '';
})
->make();
}
}

View file

@ -47,7 +47,7 @@ class ProductController extends Controller
"name" => "required|max:30",
"price" => "required|numeric|max:1000000|min:0",
"memory" => "required|numeric|max:1000000|min:5",
"cpu" => "required|numeric|max:1000000|min:5",
"cpu" => "required|numeric|max:1000000|min:0",
"swap" => "required|numeric|max:1000000|min:0",
"description" => "required",
"disk" => "required|numeric|max:1000000|min:5",
@ -103,7 +103,7 @@ class ProductController extends Controller
"name" => "required|max:30",
"price" => "required|numeric|max:1000000|min:0",
"memory" => "required|numeric|max:1000000|min:5",
"cpu" => "required|numeric|max:1000000|min:5",
"cpu" => "required|numeric|max:1000000|min:0",
"swap" => "required|numeric|max:1000000|min:0",
"description" => "required",
"disk" => "required|numeric|max:1000000|min:5",

View file

@ -8,9 +8,10 @@ use App\Models\Configuration;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Str;
class RegisterController extends Controller
{
@ -52,13 +53,21 @@ class RegisterController extends Controller
*/
protected function validator(array $data)
{
//check if ip has already made an account
$data['ip'] = session()->get('ip') ?? request()->ip();
if (User::where('ip', '=', request()->ip())->exists()) session()->put('ip', request()->ip());
if (Configuration::getValueByKey('REGISTER_IP_CHECK', 'true') == 'true') {
//check if registered cookie exists as extra defense
if (isset($_COOKIE['4b3403665fea6'])) {
$data['registered'] = env('APP_ENV') == 'local' ? false : true;
//check if ip has already made an account
$data['ip'] = session()->get('ip') ?? request()->ip();
if (User::where('ip', '=', request()->ip())->exists()) session()->put('ip', request()->ip());
return Validator::make($data, [
'name' => ['required', 'string', 'max:30', 'min:4', 'alpha_num', 'unique:users'],
'email' => ['required', 'string', 'email', 'max:64', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'g-recaptcha-response' => ['recaptcha'],
'ip' => ['unique:users'],
], [
'ip.unique' => "You have already made an account with us! Please contact support if you think this is incorrect."
]);
}
return Validator::make($data, [
@ -66,32 +75,28 @@ class RegisterController extends Controller
'email' => ['required', 'string', 'email', 'max:64', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'g-recaptcha-response' => ['recaptcha'],
'ip' => ['unique:users'],
'registered' => ['nullable', 'boolean', 'in:true']
], [
'ip.unique' => "You have already made an account with us! Please contact support if you think this is incorrect.",
'registered.in' => "You have already made an account with us! Please contact support if you think this is incorrect."
]);
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
* @return User|\Illuminate\Http\RedirectResponse
* @return User
*/
protected function create(array $data)
{
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'credits' => Configuration::getValueByKey('INITIAL_CREDITS'),
'server_limit' => Configuration::getValueByKey('INITIAL_SERVER_LIMIT'),
'credits' => Configuration::getValueByKey('INITIAL_CREDITS', 150),
'server_limit' => Configuration::getValueByKey('INITIAL_SERVER_LIMIT', 1),
'password' => Hash::make($data['password']),
]);
$response = Pterodactyl::client()->post('/application/users', [
"external_id" => (string)$user->id,
"external_id" => App::environment('local') ? Str::random(16) : (string)$user->id,
"username" => $user->name,
"email" => $user->email,
"first_name" => $user->name,
@ -103,7 +108,6 @@ class RegisterController extends Controller
if ($response->failed()) {
$user->delete();
redirect()->route('register')->with('error', 'pterodactyl error');
return $user;
}

View file

@ -18,9 +18,6 @@ class HomeController extends Controller
/** Show the application dashboard. */
public function index(Request $request)
{
//set cookie as extra layer of defense against users that make multiple accounts
setcookie('4b3403665fea6' , base64_encode(1) , time() + (20 * 365 * 24 * 60 * 60));
$usage = 0;
foreach (Auth::user()->Servers as $server){

View file

@ -12,11 +12,8 @@ use App\Models\Product;
use App\Models\Server;
use App\Notifications\ServerCreationError;
use Exception;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
@ -33,23 +30,14 @@ class ServerController extends Controller
/** Show the form for creating a new resource. */
public function create()
{
//limit
if (Auth::user()->Servers->count() >= Auth::user()->server_limit) {
return redirect()->route('servers.index')->with('error', "You've already reached your server limit!");
}
//minimum credits
if (Auth::user()->credits <= Configuration::getValueByKey('MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER' , 50)) {
return redirect()->route('servers.index')->with('error', "You do not have the required amount of credits to create a new server!");
}
if (!is_null($this->validateConfigurationRules())) return $this->validateConfigurationRules();
return view('servers.create')->with([
'products' => Product::where('disabled' , '=' , false)->orderBy('price', 'asc')->get(),
'locations' => Location::whereHas('nodes' , function ($query) {
$query->where('disabled' , '=' , false);
'products' => Product::where('disabled', '=', false)->orderBy('price', 'asc')->get(),
'locations' => Location::whereHas('nodes', function ($query) {
$query->where('disabled', '=', false);
})->get(),
'nests' => Nest::where('disabled' , '=' , false)->get(),
'nests' => Nest::where('disabled', '=', false)->get(),
]);
}
@ -57,22 +45,14 @@ class ServerController extends Controller
public function store(Request $request)
{
$request->validate([
"name" => "required|max:191",
"name" => "required|max:191",
"description" => "nullable|max:191",
"node_id" => "required|exists:nodes,id",
"egg_id" => "required|exists:eggs,id",
"product_id" => "required|exists:products,id",
"node_id" => "required|exists:nodes,id",
"egg_id" => "required|exists:eggs,id",
"product_id" => "required|exists:products,id",
]);
//limit validation
if (Auth::user()->servers()->count() >= Auth::user()->server_limit) {
return redirect()->route('servers.index')->with('error', 'Server limit reached!');
}
//minimum credits
if (Auth::user()->credits <= Configuration::getValueByKey('MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER' , 50)) {
return redirect()->route('servers.index')->with('error', "You do not have the required amount of credits to create a new server!");
}
if (!is_null($this->validateConfigurationRules())) return $this->validateConfigurationRules();
//create server
$egg = Egg::findOrFail($request->input('egg_id'));
@ -80,7 +60,7 @@ class ServerController extends Controller
$node = Node::findOrFail($request->input('node_id'));
//create server on pterodactyl
$response = Pterodactyl::createServer($server , $egg , $node);
$response = Pterodactyl::createServer($server, $egg, $node);
if (is_null($response)) return $this->serverCreationFailed($server);
if ($response->failed()) return $this->serverCreationFailed($server);
@ -88,7 +68,7 @@ class ServerController extends Controller
//update server with pterodactyl_id
$server->update([
'pterodactyl_id' => $response->json()['attributes']['id'],
'identifier' => $response->json()['attributes']['identifier']
'identifier' => $response->json()['attributes']['identifier']
]);
return redirect()->route('servers.index')->with('success', 'server created');
@ -103,13 +83,41 @@ class ServerController extends Controller
return redirect()->route('servers.index')->with('error', 'No allocations satisfying the requirements for automatic deployment were found.');
}
/**
* @return null|RedirectResponse
*/
private function validateConfigurationRules(){
//limit validation
if (Auth::user()->servers()->count() >= Auth::user()->server_limit) {
return redirect()->route('servers.index')->with('error', 'Server limit reached!');
}
//minimum credits
if (Auth::user()->credits <= Configuration::getValueByKey('MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER', 50)) {
return redirect()->route('servers.index')->with('error', "You do not have the required amount of credits to create a new server!");
}
//Required Verification for creating an server
if (Configuration::getValueByKey('FORCE_EMAIL_VERIFICATION', 'false') === 'true' && !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 (Configuration::getValueByKey('FORCE_DISCORD_VERIFICATION', 'false') === 'true' && !Auth::user()->discordUser) {
return redirect()->route('profile.index')->with('error', "You are required to link your discord account before you can create a server.");
}
return null;
}
/** Remove the specified resource from storage. */
public function destroy(Server $server)
{
try {
$server->delete();
return redirect()->route('servers.index')->with('success', 'server removed');
} catch (\Exception $e) {
} catch (Exception $e) {
return redirect()->route('servers.index')->with('error', 'An exception has occurred while trying to remove a resource');
}
}

View file

@ -2,12 +2,9 @@
namespace App\Http\Controllers;
use App\Models\Configuration;
use App\Models\PaypalProduct;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
class StoreController extends Controller
{
@ -16,9 +13,21 @@ class StoreController extends Controller
{
$isPaypalSetup = false;
if (env('PAYPAL_SECRET') && env('PAYPAL_CLIENT_ID')) $isPaypalSetup = true;
if (env('APP_ENV', 'local') == 'local') $isPaypalSetup = true;
//Required Verification for creating an server
if (Configuration::getValueByKey('FORCE_EMAIL_VERIFICATION', false) === 'true' && !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 (Configuration::getValueByKey('FORCE_DISCORD_VERIFICATION', false) === 'true' && !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' => PaypalProduct::where('disabled' , '=' , false)->orderBy('price' , 'asc')->get(),
'products' => PaypalProduct::where('disabled', '=', false)->orderBy('price', 'asc')->get(),
'isPaypalSetup' => $isPaypalSetup
]);
}

View file

@ -6,6 +6,7 @@ use Hidehalo\Nanoid\Client;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use NumberFormatter;
use Spatie\Activitylog\Traits\LogsActivity;
class Payment extends Model
@ -19,21 +20,23 @@ class Payment extends Model
* @var string[]
*/
protected $fillable = [
'id',
'user_id',
'payment_id',
'payer_id',
'payer',
'status',
'type',
'amount',
'price',
'id',
'user_id',
'payment_id',
'payer_id',
'payer',
'status',
'type',
'amount',
'price',
'currency_code',
];
public static function boot() {
public static function boot()
{
parent::boot();
static::creating(function(Payment $payment) {
static::creating(function (Payment $payment) {
$client = new Client();
$payment->{$payment->getKeyName()} = $client->generateId($size = 8);
@ -43,12 +46,14 @@ class Payment extends Model
/**
* @return BelongsTo
*/
public function User(){
public function user()
{
return $this->belongsTo(User::class);
}
public function Price()
public function formatCurrency($locale = 'en_US')
{
return number_format($this->price, 2, '.', '');
$formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
return $formatter->formatCurrency($this->price, $this->currency_code);
}
}

View file

@ -39,6 +39,10 @@ class PaypalProduct extends Model
});
}
/**
* @param string $locale
* @return string
*/
public function formatCurrency($locale = 'en_US')
{
$formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);

View file

@ -27,6 +27,21 @@ class Product extends Model
});
}
public function getHourlyPrice()
{
return ($this->price / 30) / 24;
}
public function getDailyPrice()
{
return ($this->price / 30);
}
public function getWeeklyPrice()
{
return ($this->price / 4);
}
/**
* @return BelongsTo
*/

View file

@ -124,6 +124,7 @@ class Server extends Model
return $this;
}
/**
* @return HasOne
*/

View file

@ -0,0 +1,64 @@
<?php
namespace App\Notifications;
use App\Models\Payment;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ConfirmPaymentNotification extends Notification implements ShouldQueue
{
use Queueable;
private Payment $payment;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Payment $payment)
{
$this->payment = $payment;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->subject('Payment Confirmation')
->markdown('mail.payment.confirmed' , ['payment' => $this->payment]);
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
'title' => "Payment Confirmed!",
'content' => "Payment Confirmed!",
];
}
}

View file

@ -8,6 +8,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class ServerCreationError extends Notification
{
use Queueable;
/**

View file

@ -0,0 +1,70 @@
<?php
namespace App\Notifications;
use App\Models\Configuration;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ServersSuspendedNotification extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail' , 'database'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->subject('Your servers have been suspended!')
->greeting('Your servers have been suspended!')
->line("To automatically re-enable your server/s, you need to purchase more credits.")
->action('Purchase credits', route('store.index'))
->line('If you have any questions please let us know.');
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
'title' => "Servers suspended!",
'content' => "
<h5>Your servers have been suspended!</h5>
<p>To automatically re-enable your server/s, you need to purchase more credits.</p>
<p>If you have any questions please let us know.</p>
<p>Regards,<br />" . config('app.name', 'Laravel') . "</p>
",
];
}
}

View file

@ -5,9 +5,10 @@ namespace App\Notifications;
use App\Models\Configuration;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
class WelcomeMessage extends Notification
class WelcomeMessage extends Notification implements ShouldQueue
{
use Queueable;

View file

@ -0,0 +1,38 @@
<?php
namespace Database\Factories;
use App\Models\Payment;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class PaymentFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Payment::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'payment_id' => Str::random(30),
'payer_id' => Str::random(30),
'user_id' => User::factory(),
'type' => "Credits",
'status' => "Completed",
'amount' => $this->faker->numberBetween(10, 10000),
'price' => $this->faker->numerify('##.##'),
'currency_code' => ['EUR', 'USD'][rand(0,1)],
'payer' => '{}',
];
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Database\Factories;
use App\Models\Product;
use Illuminate\Database\Eloquent\Factories\Factory;
class ProductFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Product::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->name,
'description' => $this->faker->text(60),
'price' => $this->faker->numberBetween(0 , 1000),
'memory' => $this->faker->numberBetween(32 , 1024),
'disk' => $this->faker->numberBetween(500 , 5000),
'databases' => $this->faker->numberBetween(1 , 10)
];
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Database\Factories;
use App\Models\Product;
use App\Models\Server;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class ServerFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Server::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->name,
'description' => $this->faker->text(60),
'identifier' => Str::random(30),
'pterodactyl_id' => $this->faker->numberBetween(1000000,1000000000),
'product_id' => Product::factory()
];
}
}

View file

@ -25,7 +25,9 @@ class UserFactory extends Factory
return [
'name' => $this->faker->name,
'email' => $this->faker->unique()->safeEmail,
'email_verified_at' => now(),
'credits' => $this->faker->numberBetween(0,1500),
'last_seen' => $this->faker->dateTimeBetween(now(), '+30 days'),
'email_verified_at' => $this->faker->dateTimeBetween('-30 days', now()),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
];

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdatePriceToPaymentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('payments', function (Blueprint $table) {
$table->decimal('price')->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('payments', function (Blueprint $table) {
$table->string('price')->change()->nullable();;
});
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddCurrencyCodeToPaymentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('payments', function (Blueprint $table) {
$table->string('currency_code' , 3)->default('USD')->after('price');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('payments', function (Blueprint $table) {
$table->dropColumn('currency_code');
});
}
}

View file

@ -1,70 +0,0 @@
<?php
namespace Database\Seeders;
use App\Models\Configuration;
use Illuminate\Database\Seeder;
class ConfigurationSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
//initials
Configuration::firstOrCreate([
'key' => 'INITIAL_CREDITS',
], [
'value' => '250',
'type' => 'integer',
]);
Configuration::firstOrCreate([
'key' => 'INITIAL_SERVER_LIMIT',
], [
'value' => '1',
'type' => 'integer',
]);
//verify email event
Configuration::firstOrCreate([
'key' => 'CREDITS_REWARD_AFTER_VERIFY_EMAIL',
], [
'value' => '250',
'type' => 'integer',
]);
Configuration::firstOrCreate([
'key' => 'SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL',
], [
'value' => '2',
'type' => 'integer',
]);
//verify discord event
Configuration::firstOrCreate([
'key' => 'CREDITS_REWARD_AFTER_VERIFY_DISCORD',
] , [
'value' => '375',
'type' => 'integer',
]);
Configuration::firstOrCreate([
'key' => 'SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD',
], [
'value' => '2',
'type' => 'integer',
]);
//other
Configuration::firstOrCreate([
'key' => 'MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER',
], [
'value' => '50',
'type' => 'integer',
]);
}
}

View file

@ -2,6 +2,7 @@
namespace Database\Seeders;
use Database\Seeders\Seeds\ConfigurationSeeder;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder

View file

@ -0,0 +1,21 @@
<?php
namespace Database\Seeders;
use Database\Seeders\Seeds\UserSeeder;
use Illuminate\Database\Seeder;
class DevelopmentSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$this->call([
UserSeeder::class,
]);
}
}

View file

@ -2,6 +2,10 @@
namespace Database\Seeders;
use Database\Seeders\Seeds\ProductSeeder;
use Database\Seeders\Seeds\PaypalProductSeeder;
use Database\Seeders\Seeds\ApplicationApiSeeder;
use Database\Seeders\Seeds\UsefulLinksSeeder;
use Illuminate\Database\Seeder;
class ExampleItemsSeeder extends Seeder

View file

@ -1,6 +1,6 @@
<?php
namespace Database\Seeders;
namespace Database\Seeders\Seeds;
use App\Models\ApplicationApi;
use Illuminate\Database\Seeder;

View file

@ -0,0 +1,113 @@
<?php
namespace Database\Seeders\Seeds;
use App\Models\Configuration;
use Illuminate\Database\Seeder;
class ConfigurationSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
//initials
Configuration::firstOrCreate([
'key' => 'INITIAL_CREDITS',
], [
'value' => '250',
'type' => 'integer',
'description' => 'The initial amount of credits the user starts with.'
]);
Configuration::firstOrCreate([
'key' => 'INITIAL_SERVER_LIMIT',
], [
'value' => '1',
'type' => 'integer',
'description' => 'The initial server limit the user starts with.'
]);
//verify email event
Configuration::firstOrCreate([
'key' => 'CREDITS_REWARD_AFTER_VERIFY_EMAIL',
], [
'value' => '250',
'type' => 'integer',
'description' => 'Increase in credits after the user has verified their email account.'
]);
Configuration::firstOrCreate([
'key' => 'SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL',
], [
'value' => '2',
'type' => 'integer',
'description' => 'Increase in server limit after the user has verified their email account.'
]);
//verify discord event
Configuration::firstOrCreate([
'key' => 'CREDITS_REWARD_AFTER_VERIFY_DISCORD',
], [
'value' => '375',
'type' => 'integer',
'description' => 'Increase in credits after the user has verified their discord account.'
]);
Configuration::firstOrCreate([
'key' => 'SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD',
], [
'value' => '2',
'type' => 'integer',
'description' => 'Increase in server limit after the user has verified their discord account.'
]);
//other
Configuration::firstOrCreate([
'key' => 'MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER',
], [
'value' => '50',
'type' => 'integer',
'description' => 'The minimum amount of credits the user would need to make a server.'
]);
//purchasing
Configuration::firstOrCreate([
'key' => 'SERVER_LIMIT_AFTER_IRL_PURCHASE',
], [
'value' => '10',
'type' => 'integer',
'description' => 'updates the users server limit to this amount (unless the user already has a higher server limit) after making a purchase with real money, set to 0 to ignore this.',
]);
//force email and discord verification
Configuration::firstOrCreate([
'key' => 'FORCE_EMAIL_VERIFICATION',
], [
'value' => 'false',
'type' => 'boolean',
'description' => 'Force an user to verify the email adress before creating a server / buying credits.'
]);
Configuration::firstOrCreate([
'key' => 'FORCE_DISCORD_VERIFICATION',
], [
'value' => 'false',
'type' => 'boolean',
'description' => 'Force an user to link an Discord Account before creating a server / buying credits.'
]);
//disable ip check on register
Configuration::firstOrCreate([
'key' => 'REGISTER_IP_CHECK',
], [
'value' => 'true',
'type' => 'boolean',
'description' => 'Prevent users from making multiple accounts using the same IP address'
]);
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace Database\Seeders;
namespace Database\Seeders\Seeds;
use App\Models\PaypalProduct;
use Illuminate\Database\Seeder;

View file

@ -1,6 +1,6 @@
<?php
namespace Database\Seeders;
namespace Database\Seeders\Seeds;
use App\Models\Product;
use Illuminate\Database\Seeder;

View file

@ -1,6 +1,6 @@
<?php
namespace Database\Seeders;
namespace Database\Seeders\Seeds;
use App\Models\UsefulLink;
use Illuminate\Database\Seeder;

View file

@ -0,0 +1,23 @@
<?php
namespace Database\Seeders\Seeds;
use App\Models\Server;
use App\Models\User;
use Illuminate\Database\Seeder;
class UserSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
User::factory()
->count(10)
->has(Server::factory()->count(rand(1,3)) , 'servers')
->create();
}
}

Binary file not shown.

After

(image error) Size: 140 KiB

View file

@ -33,7 +33,7 @@
@else
<div class="callout callout-danger">
<h4>No recent activity from cronjobs</h4>
<p>Are cronjobs running? <a class="text-primary" target="_blank" href="https://github.com/AVMG20/bitsec-dashboard#crontab-configuration">link</a></p>
<p>Are cronjobs running? <a class="text-primary" target="_blank" href="https://github.com/ControlPanel-gg/dashboard/wiki/Installation#crontab-configuration">Check the docs for it here</a></p>
</div>
@endif

View file

@ -40,7 +40,7 @@
<th>Key</th>
<th>Value</th>
<th>Type</th>
<th>Description</th>
<th width="600">Description</th>
<th>Created at</th>
<th></th>
</tr>

View file

@ -28,48 +28,55 @@
<div class="card-header">
<h5 class="card-title"><i class="fas fa-money-bill-wave mr-2"></i>Payments</h5>
</div>
<div class="card-body table-responsive">
<table class="table table-striped">
<div class="card-body table-responsive">
<table id="datatable" class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>User</th>
<th>Type</th>
<th>Amount</th>
<th>Price</th>
<th>Payment_ID</th>
<th>Payer_ID</th>
<th>Created at</th>
</tr>
<tr>
<th>ID</th>
<th>User</th>
<th>Type</th>
<th>Amount</th>
<th>Price</th>
<th>Payment_ID</th>
<th>Payer_ID</th>
<th>Created at</th>
</tr>
</thead>
<tbody>
@foreach($payments as $payment)
<tr>
<td>{{$payment->id}}</td>
<td>{{$payment->User->name}}</td>
<td>{{$payment->type}}</td>
<td><i class="fa fa-coins mr-2"></i>{{$payment->amount}}</td>
<td>{{$payment->Price()}}</td>
<td>{{$payment->payment_id}}</td>
<td>{{$payment->payer_id}}</td>
<td>{{$payment->created_at->diffForHumans()}}</td>
</tr>
@endforeach
</tbody>
</table>
<div class="float-right">
{!! $payments->links() !!}
</div>
</div>
</div>
</div>
</div>
<!-- END CUSTOM CONTENT -->
</div>
</section>
<!-- END CONTENT -->
<script>
document.addEventListener("DOMContentLoaded", function () {
$('#datatable').DataTable({
processing: true,
serverSide: true,
stateSave: true,
ajax: "{{route('admin.payments.datatable')}}",
columns: [
{data: 'id' , name : 'payments.id'},
{data: 'user', sortable: false},
{data: 'type'},
{data: 'amount'},
{data: 'price'},
{data: 'payment_id'},
{data: 'payer_id'},
{data: 'created_at'},
],
fnDrawCallback: function( oSettings ) {
$('[data-toggle="popover"]').popover();
}
});
});
</script>
@endsection

View file

@ -0,0 +1,18 @@
@component('mail::message')
# Thank you for your purchase!
Your payment has been confirmed; Your credit balance has been updated.
# Details
___
### Payment ID: **{{$payment->id}}**
### Status: **{{$payment->status}}**
### Price: **{{$payment->formatCurrency()}}**
### Type: **{{$payment->type}}**
### Amount: **{{$payment->amount}}**
### Balance: **{{$payment->user->credits}}**
### User ID: **{{$payment->user_id}}**
<br>
Thanks,<br>
{{ config('app.name') }}
@endcomponent

View file

@ -67,8 +67,10 @@
@foreach($locations as $location)
<optgroup label="{{$location->name}}">
@foreach($location->nodes as $nodes)
<option value="{{$nodes->id}}">{{$nodes->name}}</option>
@foreach($location->nodes as $node)
@if(!$node->disabled)
<option value="{{$node->id}}">{{$node->name}}</option>
@endif
@endforeach
</optgroup>
@endforeach

View file

@ -44,12 +44,12 @@
<i class="fas fa-ellipsis-v fa-sm fa-fw text-white-50"></i>
</a>
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in" aria-labelledby="dropdownMenuLink">
<a href="{{env('PTERODACTYL_URL' , 'http://localhost')}}/server/{{$server->identifier}}" class="dropdown-item text-info"><i title="manage" class="fas fa-tasks mr-2"></i><span>Manage</span></a>
<a href="{{env('PHPMYADMIN_URL' , 'http://localhost')}}" class="dropdown-item text-info"><i title="manage" class="fas fa-database mr-2"></i><span>Database</span></a>
<a href="{{env('PTERODACTYL_URL' , 'http://localhost')}}/server/{{$server->identifier}}" target="__blank" class="dropdown-item text-info"><i title="manage" class="fas fa-tasks mr-2"></i><span>Manage</span></a>
<a href="{{env('PHPMYADMIN_URL' , 'http://localhost')}}" class="dropdown-item text-info" target="__blank"><i title="manage" class="fas fa-database mr-2"></i><span>Database</span></a>
<form method="post" onsubmit="return submitResult();" action="{{route('servers.destroy' , $server->id)}}">
@csrf
@method('DELETE')
<button class="dropdown-item text-danger"><i title="delete" class="fas fa-trash mr-2"></i><span>Delete</span></button>
<button class="dropdown-item text-danger"><i title="delete" class="fas fa-trash mr-2"></i><span>Delete server</span></button>
</form>
<div class="dropdown-divider"></div>
<span class="dropdown-item"><i title="Created at" class="fas fa-sync-alt mr-2"></i><span>{{$server->created_at->isoFormat('LL')}}</span></span>
@ -66,7 +66,7 @@
<td>{{$server->product->cpu}} %</td>
</tr>
<tr>
<td>Ram</td>
<td>RAM</td>
<td>{{$server->product->memory}} MB</td>
</tr>
<tr>
@ -86,8 +86,8 @@
<div class="card-footer d-flex justify-content-between">
<a href="{{env('PTERODACTYL_URL' , 'http://localhost')}}/server/{{$server->identifier}}" class="btn btn-info mx-3 w-100"><i class="fas fa-tasks mr-2"></i>Manage</a>
<a href="{{env('PHPMYADMIN_URL' , 'http://localhost')}}" class="btn btn-info mx-3 w-100"><i class="fas fa-database mr-2"></i>Database</a>
<a href="{{env('PTERODACTYL_URL' , 'http://localhost')}}/server/{{$server->identifier}}" target="__blank" class="btn btn-info mx-3 w-100"><i class="fas fa-tasks mr-2"></i>Manage</a>
<a href="{{env('PHPMYADMIN_URL' , 'http://localhost')}}" target="__blank" class="btn btn-info mx-3 w-100" ><i class="fas fa-database mr-2"></i>Database</a>
</div>
</div>

View file

@ -59,10 +59,10 @@
@else
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
<h4><i class="icon fa fa-ban"></i> @if($products->count() == 0) The store is temporarily
disabled! @else The store is not correctly configured! @endif
<h4><i class="icon fa fa-ban"></i> @if($products->count() == 0) There are no store products! @else The store is not correctly configured! @endif
</h4>
</div>
@endif

View file

@ -88,6 +88,7 @@ Route::middleware('auth')->group(function () {
'store' => 'paypalProduct',
]);
Route::get('payments/datatable', [PaymentController::class, 'datatable'])->name('payments.datatable');
Route::get('payments', [PaymentController::class, 'index'])->name('payments.index');
Route::get('nodes/datatable', [NodeController::class, 'datatable'])->name('nodes.datatable');
@ -101,6 +102,7 @@ Route::middleware('auth')->group(function () {
Route::get('configurations/datatable', [ConfigurationController::class, 'datatable'])->name('configurations.datatable');
Route::patch('configurations/updatevalue', [ConfigurationController::class, 'updatevalue'])->name('configurations.updatevalue');
Route::resource('configurations', ConfigurationController::class);
Route::resource('configurations', ConfigurationController::class);
Route::patch('settings/update/icons', [SettingsController::class , 'updateIcons'])->name('settings.update.icons');
Route::resource('settings', SettingsController::class)->only('index');