Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Location Addon Implementation #950

Merged
merged 11 commits into from
Jun 12, 2024
Merged
6 changes: 3 additions & 3 deletions .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ body:
validations:
required: false
- type: textarea
id: controlpanel-logs
id: ctrlpanel-logs
attributes:
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`
label: CtrlPanel 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/ctrlpanel/storage/logs/laravel.log | nc pteropaste.com 99`
render: Shell
- type: textarea
id: additional-info
Expand Down
6 changes: 3 additions & 3 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ At this time, we only accept vulnerability reports through GitHub Advisories. We

## Supported Versions

- Latests
### CtrlPanel Versions

### ControlPanel Versions
- Latests

We strongly recommend using or upgrading to the latest version of ControlPanel to ensure you have access to the latest security fixes and enhancements.
We strongly recommend using or upgrading to the latest version of CtrlPanel to ensure you have access to the latest security fixes and enhancements.
6 changes: 4 additions & 2 deletions app/Http/Controllers/Auth/ForgotPasswordController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ public function __construct()
$this->middleware('guest');
}

protected function validateEmail(Request $request, GeneralSettings $general_settings)
protected function validateEmail(Request $request)
{
$this->validate($request, [
'email' => ['required', 'string', 'email', 'max:255'],
]);

if ($general_settings->recaptcha_enabled) {
$recaptcha_enabled = app(GeneralSettings::class)->recaptcha_enabled;

if ($recaptcha_enabled) {
$this->validate($request, [
'g-recaptcha-response' => 'required|recaptcha',
]);
Expand Down
51 changes: 41 additions & 10 deletions app/Http/Controllers/ProductController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@
use App\Models\Pterodactyl\Location;
use App\Models\Pterodactyl\Node;
use App\Models\Product;
use App\Models\User;
use App\Notifications\DynamicNotification;
use App\Settings\PterodactylSettings;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\RateLimiter;

class ProductController extends Controller
{
{
private $pterodactyl;

public function __construct(PterodactylSettings $ptero_settings)
Expand Down Expand Up @@ -93,34 +98,60 @@ public function getLocationsBasedOnEgg(Request $request, Egg $egg)
}
});

if($locations->isEmpty()){
// Rate limit the node full notification to 1 attempt per 30 minutes
RateLimiter::attempt(
key: 'nodes-full-warning',
maxAttempts: 1,
callback: function() {
// get admin role and check users
$users = User::query()->where('role', '=', '1')->get();
Notification::send($users,new DynamicNotification(['mail'],[],
mail: (new MailMessage)->subject('Attention! All of the nodes are full!')->greeting('Attention!')->line('All nodes are full, please add more nodes')));
},
decaySeconds: 5
);
}

return $locations;
}

/**
* @param Node $node
* @param Int $location
* @param Egg $egg
* @return Collection|JsonResponse
*/
public function getProductsBasedOnNode(Egg $egg, Node $node)
public function getProductsBasedOnLocation(Egg $egg, Int $location)
{
if (is_null($egg->id) || is_null($node->id)) {
return response()->json('node and egg id is required', '400');
if (is_null($egg->id) || is_null($location)) {
return response()->json('location and egg id is required', '400');
}

// Get all nodes in this location
$nodes = Node::query()
->where('location_id', '=', $location)
->get();

$products = Product::query()
->where('disabled', '=', false)
->whereHas('nodes', function (Builder $builder) use ($node) {
$builder->where('id', '=', $node->id);
->whereHas('nodes', function (Builder $builder) use ($nodes) {
$builder->whereIn('id', $nodes->map(function ($node) {
return $node->id;
}));
})
->whereHas('eggs', function (Builder $builder) use ($egg) {
$builder->where('id', '=', $egg->id);
})
->get();

$pteroNode = $this->pterodactyl->getNode($node->id);
// Instead of the old node check, we will check if the product fits in any given node in the location
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;
$product->doesNotFit = false;
foreach ($nodes as $node) {
$pteroNode = $this->pterodactyl->getNode($node->id);
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;
}
}
}

Expand Down
65 changes: 49 additions & 16 deletions app/Http/Controllers/ServerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public function create(UserSettings $user_settings, ServerSettings $server_setti
'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,
'location_description_enabled' => $server_settings->location_description_enabled,
'store_enabled' => $general_settings->store_enabled
]);
}
Expand All @@ -142,13 +143,10 @@ private function validateConfigurationRules(UserSettings $user_settings, ServerS
$product = Product::findOrFail(FacadesRequest::input('product'));

// Get node resource allocation info
$node = $product->nodes()->findOrFail(FacadesRequest::input('node'));
$nodeName = $node->name;

// Check if node has enough memory and disk space
$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."));
$location = FacadesRequest::input('location');
$availableNode = $this->getAvailableNode($location, $product);
if (!$availableNode) {
return redirect()->route('servers.index')->with('error', __("The chosen location doesn't have the required memory or disk left to allocate this product."));
}

// Min. Credits
Expand Down Expand Up @@ -180,7 +178,7 @@ private function validateConfigurationRules(UserSettings $user_settings, ServerS
/** Store a newly created resource in storage. */
public function store(Request $request, UserSettings $user_settings, ServerSettings $server_settings, GeneralSettings $generalSettings)
{
/** @var Node $node */
/** @var Location $location */
/** @var Egg $egg */
/** @var Product $product */
$validate_configuration = $this->validateConfigurationRules($user_settings, $server_settings, $generalSettings);
Expand All @@ -191,15 +189,23 @@ public function store(Request $request, UserSettings $user_settings, ServerSetti

$request->validate([
'name' => 'required|max:191',
'node' => 'required|exists:nodes,id',
'location' => 'required|exists:locations,id',
'egg' => 'required|exists:eggs,id',
'product' => 'required|exists:products,id',
]);

//get required resources
// Get the product and egg
$product = Product::query()->findOrFail($request->input('product'));
$egg = $product->eggs()->findOrFail($request->input('egg'));
$node = $product->nodes()->findOrFail($request->input('node'));

// Get an available node
$location = $request->input('location');
$availableNode = $this->getAvailableNode($location, $product);
$node = Node::query()->find($availableNode);

if(!$node) {
return redirect()->route('servers.index')->with('error', __("No nodes satisfying the requirements for automatic deployment on this location were found."));
}

$server = $request->user()->servers()->create([
'name' => $request->input('name'),
Expand Down Expand Up @@ -317,7 +323,7 @@ public function show(Server $server, ServerSettings $server_settings, GeneralSet
})
->get();

// Set the each product eggs array to just contain the eggs name
// Set each product eggs array to just contain the eggs name
foreach ($products as $product) {
$product->eggs = $product->eggs->pluck('name')->toArray();
if ($product->memory - $currentProduct->memory > ($pteroNode['memory'] * ($pteroNode['memory_overallocate'] + 100) / 100) - $pteroNode['allocated_resources']['memory'] || $product->disk - $currentProduct->disk > ($pteroNode['disk'] * ($pteroNode['disk_overallocate'] + 100) / 100) - $pteroNode['allocated_resources']['disk']) {
Expand All @@ -329,7 +335,8 @@ public function show(Server $server, ServerSettings $server_settings, GeneralSet
'server' => $server,
'products' => $products,
'server_enable_upgrade' => $server_settings->enable_upgrade,
'credits_display_name' => $general_settings->credits_display_name
'credits_display_name' => $general_settings->credits_display_name,
'location_description_enabled' => $server_settings->location_description_enabled,
]);
}

Expand Down Expand Up @@ -357,8 +364,8 @@ public function upgrade(Server $server, Request $request)
// Check if node has enough memory and disk space
$requireMemory = $newProduct->memory - $oldProduct->memory;
$requiredisk = $newProduct->disk - $oldProduct->disk;
$checkResponse = $this->pterodactyl->checkNodeResources($node, $requireMemory, $requiredisk);
if ($checkResponse == false) {
$nodeFree = $this->pterodactyl->checkNodeResources($node, $requireMemory, $requiredisk);
if (!$nodeFree) {
return redirect()->route('servers.index')->with('error', __("The node '" . $nodeName . "' doesn't have the required memory or disk left to upgrade the server."));
}

Expand Down Expand Up @@ -394,7 +401,7 @@ public function upgrade(Server $server, Request $request)

// 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
// Update the server on CtrlPanel
$server->update([
'product_id' => $newProduct->id,
'updated_at' => now(),
Expand All @@ -413,4 +420,30 @@ public function upgrade(Server $server, Request $request)
return redirect()->route('servers.show', ['server' => $server->id])->with('error', __('Not Enough Balance for Upgrade'));
}
}

/**
* @param string $location
* @param Product $product
* @return int | null Node ID
*/
private function getAvailableNode(string $location, Product $product)
{
$collection = Node::query()->where('location_id', $location)->get();

// loop through nodes and check if the node has enough resources
foreach ($collection as $node) {
// Check if the node has enough memory and disk space
$freeNode = $this->pterodactyl->checkNodeResources($node, $product->memory, $product->disk);
// Remove the node from the collection if it doesn't have enough resources
if (!$freeNode) {
$collection->forget($node['id']);
}
}

if($collection->isEmpty()) {
return null;
}

return $collection->first()['id'];
}
}
2 changes: 1 addition & 1 deletion app/Models/Pterodactyl/Location.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public static function syncLocations()
],
[
'name' => $location['name'],
'description' => $location['name'],
'description' => $location['description'],
]
);
}
Expand Down
7 changes: 7 additions & 0 deletions app/Settings/ServerSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class ServerSettings extends Settings
public int $allocation_limit;
public bool $creation_enabled;
public bool $enable_upgrade;
public bool $location_description_enabled;

public static function group(): string
{
Expand All @@ -25,6 +26,7 @@ public static function getValidations()
'allocation_limit' => 'required|integer|min:0',
'creation_enabled' => 'nullable|string',
'enable_upgrade' => 'nullable|string',
'location_description_enabled' => 'nullable|string',
];
}

Expand Down Expand Up @@ -52,6 +54,11 @@ public static function getOptionInputData()
'type' => 'boolean',
'description' => 'Enable the server upgrade feature.',
],
'location_description_enabled' => [
'label' => 'Enable Location Description',
'type' => 'boolean',
'description' => 'Enable the location description field on the server creation page.',
],
];
}
}
2 changes: 1 addition & 1 deletion app/Traits/Invoiceable.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public function createInvoice(Payment $payment, ShopProduct $shopProduct, Invoic


$invoice = DailyInvoice::make()
->template('controlpanel')
->template('ctrlpanel')
->name(__("Invoice"))
->buyer($customer)
->seller($seller)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('servers', function (Blueprint $table) {
$table->text('description')->nullable()->change();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('servers', function (Blueprint $table) {
$table->string('description')->nullable()->change();
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('locations', function (Blueprint $table) {
$table->text('description')->change();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('locations', function (Blueprint $table) {
$table->string('description')->change();
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

use Spatie\LaravelSettings\Migrations\SettingsMigration;

return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('server.location_description_enabled',false);
}

public function down(): void
{
$this->migrator->delete('server.location_description_enabled');
}
};
Loading