new server create page w.i.p

This commit is contained in:
AVMG20 2021-11-06 01:56:57 +01:00
parent 75622d3958
commit 16a7d174e9
16 changed files with 14269 additions and 233 deletions

View file

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers;
use App\Models\Egg;
use App\Models\Node;
use App\Models\Product;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
class ProductController extends Controller
{
/**
* @description get product locations based on selected egg
* @param Request $request
* @param Egg $egg
* @return Collection|JsonResponse
*/
public function getNodesBasedOnEgg(Request $request, Egg $egg)
{
if (is_null($egg->id)) return response()->json('egg id is required', '400');
#get products that include this egg
$products = Product::query()->with('nodes')->whereHas('eggs', function (Builder $builder) use ($egg) {
$builder->where('id', '=', $egg->id);
})->get();
$nodes = collect();
#filter unique nodes
$products->each(function (Product $product) use ($nodes) {
$product->nodes->each(function (Node $node) use ($nodes) {
if (!$nodes->contains('id', $node->id) && !$node->disabled) {
$nodes->add($node);
}
});
});
return $nodes;
}
/**
* @param Node $node
* @return Collection|JsonResponse
*/
public function getProductsBasedOnNode(Node $node)
{
if (is_null($node->id)) return response()->json('node id is required', '400');
return Product::query()->whereHas('nodes', function (Builder $builder) use ($node) {
$builder->where('id' , '=' , $node->id);
})->get();
}
}

View file

@ -5,7 +5,6 @@ namespace App\Http\Controllers;
use App\Classes\Pterodactyl;
use App\Models\Configuration;
use App\Models\Egg;
use App\Models\Location;
use App\Models\Nest;
use App\Models\Node;
use App\Models\Product;
@ -16,7 +15,6 @@ use Illuminate\Http\Client\Response;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request as FacadesRequest;
class ServerController extends Controller
@ -35,56 +33,14 @@ class ServerController extends Controller
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);
})->get(),
'nests' => Nest::where('disabled', '=', false)->get(),
'productCount' => Product::query()->where('disabled', '=', false)->count(),
'nodeCount' => Node::query()->where('disabled', '=', false)->count(),
'nests' => Nest::query()->where('disabled', '=', false)->get(),
'eggs' => Egg::query()->where('disabled', '=', false)->get(),
'minimum_credits' => Configuration::getValueByKey('MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER', 50)
]);
}
/** Store a newly created resource in storage. */
public function store(Request $request)
{
if (!is_null($this->validateConfigurationRules())) return $this->validateConfigurationRules();
$request->validate([
"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"
]);
//get required resources
$egg = Egg::findOrFail($request->input('egg_id'));
$node = Node::findOrFail($request->input('node_id'));
$server = Auth::user()->servers()->create($request->all());
//get free allocation ID
$allocationId = Pterodactyl::getFreeAllocationId($node);
if (!$allocationId) return $this->noAllocationsError($server);
//create server on pterodactyl
$response = Pterodactyl::createServer($server, $egg, $allocationId);
if ($response->failed()) return $this->serverCreationFailed($response, $server);
//update server with pterodactyl_id
$server->update([
'pterodactyl_id' => $response->json()['attributes']['id'],
'identifier' => $response->json()['attributes']['identifier']
]);
if (Configuration::getValueByKey('SERVER_CREATE_CHARGE_FIRST_HOUR', 'true') == 'true') {
if (Auth::user()->credits >= $server->product->getHourlyPrice()) {
Auth::user()->decrement('credits', $server->product->getHourlyPrice());
}
}
return redirect()->route('servers.index')->with('success', 'server created');
}
/**
* @return null|RedirectResponse
*/
@ -121,17 +77,46 @@ class ServerController extends Controller
return null;
}
/** Remove the specified resource from storage. */
public function destroy(Server $server)
/** Store a newly created resource in storage. */
public function store(Request $request)
{
try {
$server->delete();
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() . '"');
}
}
if (!is_null($this->validateConfigurationRules())) return $this->validateConfigurationRules();
$request->validate([
"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"
]);
//get required resources
$egg = Egg::findOrFail($request->input('egg_id'));
$node = Node::findOrFail($request->input('node_id'));
$server = Auth::user()->servers()->create($request->all());
//get free allocation ID
$allocationId = Pterodactyl::getFreeAllocationId($node);
if (!$allocationId) return $this->noAllocationsError($server);
//create server on pterodactyl
$response = Pterodactyl::createServer($server, $egg, $allocationId);
if ($response->failed()) return $this->serverCreationFailed($response, $server);
//update server with pterodactyl_id
$server->update([
'pterodactyl_id' => $response->json()['attributes']['id'],
'identifier' => $response->json()['attributes']['identifier']
]);
if (Configuration::getValueByKey('SERVER_CREATE_CHARGE_FIRST_HOUR', 'true') == 'true') {
if (Auth::user()->credits >= $server->product->getHourlyPrice()) {
Auth::user()->decrement('credits', $server->product->getHourlyPrice());
}
}
return redirect()->route('servers.index')->with('success', 'server created');
}
/**
* return redirect with error
@ -158,4 +143,15 @@ class ServerController extends Controller
return redirect()->route('servers.index')->with('error', json_encode($response->json()));
}
/** 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) {
return redirect()->route('servers.index')->with('error', 'An exception has occurred while trying to remove a resource "' . $e->getMessage() . '"');
}
}
}

View file

@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Egg extends Model
{
@ -85,5 +86,4 @@ class Egg extends Model
{
return $this->belongsToMany(Product::class);
}
}

View file

@ -15,7 +15,6 @@ class Location extends Model
public $guarded = [];
public function nodes(){
return $this->hasMany(Node::class , 'location_id' , 'id');
}
@ -39,4 +38,5 @@ class Location extends Model
self::firstOrCreate(['id' => $location['id']] , $location);
}
}
}

View file

@ -12,7 +12,8 @@ use Spatie\Activitylog\Traits\LogsActivity;
class Product extends Model
{
use HasFactory, LogsActivity;
use HasFactory;
use LogsActivity;
public $incrementing = false;

13843
package-lock.json generated

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5
resources/js/alpine.js vendored Normal file

File diff suppressed because one or more lines are too long

1
resources/js/app.js vendored
View file

@ -3,3 +3,4 @@ require('./slim.kickstart.min')
require('./bootstrap');

View file

@ -18,10 +18,10 @@ try {
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
//
// window.axios = require('axios');
//
// window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening

View file

@ -13,7 +13,7 @@
<li class="breadcrumb-item"><a href="{{ route('home') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.products.index') }}">Products</a></li>
<li class="breadcrumb-item"><a class="text-muted"
href="{{ route('admin.products.create') }}">Create</a>
href="{{ route('admin.products.create') }}">Create</a>
</li>
</ol>
</div>
@ -54,9 +54,9 @@
class="form-control @error('name') is-invalid @enderror"
required="required">
@error('name')
<div class="invalid-feedback">
{{ $message }}
</div>
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
@ -67,9 +67,9 @@
class="form-control @error('price') is-invalid @enderror"
required="required">
@error('price')
<div class="invalid-feedback">
{{ $message }}
</div>
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
@ -81,9 +81,9 @@
class="form-control @error('memory') is-invalid @enderror"
required="required">
@error('memory')
<div class="invalid-feedback">
{{ $message }}
</div>
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
@ -94,9 +94,9 @@
class="form-control @error('cpu') is-invalid @enderror"
required="required">
@error('cpu')
<div class="invalid-feedback">
{{ $message }}
</div>
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
@ -107,9 +107,9 @@
class="form-control @error('swap') is-invalid @enderror"
required="required">
@error('swap')
<div class="invalid-feedback">
{{ $message }}
</div>
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
@ -123,9 +123,9 @@
class="form-control @error('description') is-invalid @enderror"
required="required">{{$product->description ?? old('description')}}</textarea>
@error('description')
<div class="invalid-feedback">
{{ $message }}
</div>
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
@ -138,9 +138,9 @@
class="form-control @error('disk') is-invalid @enderror"
required="required">
@error('disk')
<div class="invalid-feedback">
{{ $message }}
</div>
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
@ -150,13 +150,13 @@
data-content="Setting to -1 will use the value from configuration."
class="fas fa-info-circle"></i></label>
<input value="{{ old('minimum_credits') ?? -1 }}" id="minimum_credits"
name="minimum_credits" type="number"
class="form-control @error('minimum_credits') is-invalid @enderror"
required="required">
name="minimum_credits" type="number"
class="form-control @error('minimum_credits') is-invalid @enderror"
required="required">
@error('minimum_credits')
<div class="invalid-feedback">
{{ $message }}
</div>
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
@ -167,9 +167,9 @@
class="form-control @error('io') is-invalid @enderror"
required="required">
@error('io')
<div class="invalid-feedback">
{{ $message }}
</div>
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<div class="form-group">
@ -180,9 +180,9 @@
class="form-control @error('databases') is-invalid @enderror"
required="required">
@error('databases')
<div class="invalid-feedback">
{{ $message }}
</div>
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<div class="form-group">
@ -193,9 +193,9 @@
class="form-control @error('backups') is-invalid @enderror"
required="required">
@error('backups')
<div class="invalid-feedback">
{{ $message }}
</div>
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<div class="form-group">
@ -206,9 +206,9 @@
class="form-control @error('allocations') is-invalid @enderror"
required="required">
@error('allocations')
<div class="invalid-feedback">
{{ $message }}
</div>
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
@ -292,14 +292,9 @@
<!-- END CONTENT -->
<script>
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function () {
$('[data-toggle="popover"]').popover();
$('.custom-select').select2();
});
</script>
<script>
document.addEventListener('DOMContentLoaded', (event) => {
})
</script>
$('.custom-select').select2();
@endsection

View file

@ -10,6 +10,8 @@
href="{{\Illuminate\Support\Facades\Storage::disk('public')->exists('favicon.ico') ? asset('storage/favicon.ico') : asset('favicon.ico')}}"
type="image/x-icon">
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
{{-- <link rel="stylesheet" href="{{asset('css/adminlte.min.css')}}">--}}
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs4/dt-1.10.24/datatables.min.css"/>

View file

@ -13,7 +13,7 @@
<li class="breadcrumb-item"><a href="{{ route('home') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('servers.index') }}">Servers</a>
<li class="breadcrumb-item"><a class="text-muted"
href="{{ route('servers.create') }}">Create</a>
href="{{ route('servers.create') }}">Create</a>
</li>
</ol>
</div>
@ -23,130 +23,291 @@
<!-- END CONTENT HEADER -->
<!-- MAIN CONTENT -->
<section class="content">
<div class="container-fluid">
<section x-data="serverApp()" class="content">
<div class="container">
<!-- CUSTOM CONTENT -->
<div class="row justify-content-center">
<div class="card col-lg-8 col-md-12 mb-5">
<div class="card-header">
<h5 class="card-title"><i class="fa fa-server mr-2"></i>Create Server</h5>
</div>
<div class="card-body">
<form method="post" action="{{ route('servers.store') }}">
<!-- FORM -->
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<div class="card-title">{{__('Server configuration')}}</div>
</div>
@if($productCount === 0 || $nodeCount === 0 || count($nests) === 0 || count($eggs) === 0 )
<div class="alert alert-danger p-2 m-2">
<h5><i class="icon fas fa-exclamation-circle"></i>Error!</h5>
<ul>
@if($productCount === 0 )
<li> {{__('No products available!')}}</li>
@endif
@if($nodeCount === 0 )
<li>{{__('No nodes available!')}}</li>
@endif
@if(count($nests) === 0 )
<li>{{__('No nests available!')}}</li>
@endif
@if(count($eggs) === 0 )
<li>{{__('No eggs available!')}}</li>
@endif
</ul>
</div>
@endif
<div x-show="loading" class="overlay dark">
<i class="fas fa-2x fa-sync-alt"></i>
</div>
<div class="card-body">
@csrf
<div class="form-group">
<label for="name">* Name</label>
<input id="name" name="name" type="text" required="required"
class="form-control @error('name') is-invalid @enderror">
<label for="name">{{__('Name')}}</label>
<input x-model="name" id="name" name="name" type="text" required="required"
class="form-control @error('name') is-invalid @enderror">
@error('name')
<div class="invalid-feedback">
Please fill out this field.
</div>
<div class="invalid-feedback">
Please fill out this field.
</div>
@enderror
</div>
<div class="form-group">
<label for="description">Description</label>
<input id="description" name="description" type="text"
class="form-control @error('description') is-invalid @enderror">
@error('description')
<div class="invalid-feedback">
Please fill out this field.
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="nest">{{__('Software')}}</label>
<select class="custom-select"
name="nest"
id="nest"
x-model="selectedNest"
@change="setNests(); $refs.egg.selectedIndex = '0'">
<option selected disabled
value="null">{{count($nests) > 0 ? __('Please select software..') : __('---')}}</option>
@foreach ($nests as $nest)
<option value="{{ $nest->id }}">{{ $nest->name }}</option>
@endforeach
</select>
</div>
@enderror
</div>
<div class="form-group">
<label for="location_id">* Server location</label>
<div>
<select id="node_id" name="node_id" required="required"
class="custom-select @error('node_id') is-invalid @enderror">
<option selected disabled hidden value="">Please Select ...</option>
@foreach ($locations as $location)
<optgroup label="{{ $location->name }}">
@foreach ($location->nodes as $node)
@if (!$node->disabled)
<option value="{{ $node->id }}">{{ $node->name }}</option>
@endif
@endforeach
</optgroup>
@endforeach
</select>
</div>
@error('node_id')
<div class="invalid-feedback">
Please fill out this field.
<div class="col-md-6">
<div class="form-group">
<label for="egg">{{__('Configuration')}}</label>
<div>
<select id="egg"
name="egg"
x-ref="egg"
:disabled="eggs.length == 0"
x-model="selectedEgg"
@change="fetchNodes(); $refs.node.selectedIndex = '0'"
required="required"
class="custom-select">
<option x-text="getEggInputText()"
selected disabled hidden value="null"></option>
<template x-for="egg in eggs" :key="egg.id">
<option x-text="egg.name" :value="egg.id"></option>
</template>
</select>
</div>
</div>
@enderror
</div>
<div class="form-group">
<label for="egg_id">* Server configuration</label>
<div>
<select id="egg_id" name="egg_id" required="required"
class="custom-select @error('egg_id') is-invalid @enderror">
<option selected disabled hidden value="">Please Select ...</option>
@foreach ($nests as $nest)
<optgroup label="{{ $nest->name }}">
@foreach ($nest->eggs as $egg)
<option value="{{ $egg->id }}">{{ $egg->name }}</option>
@endforeach
</optgroup>
@endforeach
</select>
</div>
@error('egg_id')
<div class="invalid-feedback">
Please fill out this field.
</div>
@enderror
</div>
<div class="form-group">
<label for="product_id">* Resource Configuration</label>
<div>
<select id="product_id" name="product_id" required="required"
class="custom-select @error('product_id') is-invalid @enderror">
<option selected disabled hidden value="">Please Select...</option>
@foreach ($products as $product)
<option value="{{ $product->id }}" @if ($product->minimum_credits == -1 && Auth::user()->credits >= $minimum_credits)
@elseif ($product->minimum_credits != -1 && Auth::user()->credits >=
$product->minimum_credits)
@else
disabled
@endif
>{{ $product->name }}
({{ $product->description }})
</option>
@endforeach
</select>
</div>
<label for="node">{{__('Node')}}</label>
<select name="node"
id="node"
x-ref="node"
x-model="selectedNode"
:disabled="!fetchedNodes"
@change="fetchProducts();"
class="custom-select">
<option
x-text="getNodeInputText()"
disabled selected value="null"></option>
<template x-for="node in nodes" :key="node.id">
<option x-text="node.name" :value="node.id"></option>
</template>
</select>
</div>
@error('product_id')
<div class="invalid-feedback">
Please fill out this field.
<div class="form-group">
<label for="product">{{__('Resources')}}</label>
<select name="product"
id="product"
x-ref="product"
:disabled="!fetchedProducts"
x-model="selectedProduct"
class="custom-select">
<option
x-text="getProductInputText()"
disabled selected value="null"></option>
<template x-for="product in products" :key="product.id">
<option x-text="product.name + ' (' + product.description + ')'"
:value="product.id"></option>
</template>
</select>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card">
<div class="card-body">
<h4 class="d-flex justify-content-between align-items-center mb-3">
<span class="text-muted">{{__('Server details')}}</span>
</h4>
<ul class="list-group mb-3">
<li class="list-group-item d-flex justify-content-between lh-condensed">
<div>
<h6 class="my-0">{{__('Software')}}</h6>
<small class="text-muted">Brief description</small>
</div>
@enderror
</div>
<div class="form-group text-right">
<input type="submit" class="btn btn-primary mt-3" value="Submit"
onclick="this.disabled=true;this.value='Creating, please wait...';this.form.submit();">
</div>
</form>
</li>
<li class="list-group-item d-flex justify-content-between lh-condensed">
<div>
<h6 class="my-0">{{__('Configuration')}}</h6>
<small class="text-muted">Brief description</small>
</div>
</li>
<li
class="list-group-item d-flex justify-content-between lh-condensed">
<div>
<h6 class="my-0">{{__('Node')}}</h6>
<small class="text-muted">Brief description</small>
</div>
</li>
<li
class="list-group-item d-flex justify-content-between lh-condensed">
<div>
<h6 class="my-0">{{__('Resources')}}</h6>
<small class="text-muted">Brief description</small>
</div>
</li>
</ul>
<ul x-show="selectedProduct" class="list-group">
<li class="list-group-item d-flex justify-content-between">
<span>{{CREDITS_DISPLAY_NAME}} {{__('per month')}}</span>
<strong x-text="selectedProduct"></strong>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- END CUSTOM CONTENT -->
<!-- END FORM -->
</div>
</section>
<!-- END CONTENT -->
<script>
function serverApp() {
return {
loading: false,
fetchedNodes: false,
fetchedProducts: false,
name: null,
selectedNest: null,
selectedEgg: null,
selectedNode: null,
selectedProduct: null,
nests: {!! $nests !!},
eggsSave:{!! $eggs !!}, //store back-end eggs
eggs: [],
nodes: [],
products: [],
/**
* @description set available eggs based on the selected nest
* @note called whenever a nest is selected
* @see selectedNest
*/
setNests() {
this.fetchedNodes = false;
this.fetchedProducts = false;
this.nodes = [];
this.products = [];
this.eggs = this.eggsSave.filter(egg => egg.nest_id == this.selectedNest)
},
/**
* @description fetch all available locations based on the selected egg
* @note called whenever a server configuration is selected
* @see selectedEg
*/
async fetchNodes() {
this.loading = true;
this.fetchedNodes = false;
this.fetchedProducts = false;
this.nodes = [];
this.products = [];
let response = await axios.get(`{{route('products.nodes.egg')}}/${this.selectedEgg}`)
.catch(console.error)
this.fetchedNodes = true;
this.nodes = response.data
this.loading = false;
},
/**
* @description fetch all available products based on the selected node
* @note called whenever a node is selected
* @see selectedNode
*/
async fetchProducts() {
this.loading = true;
this.fetchedProducts = false;
this.products = [];
let response = await axios.get(`{{route('products.products.node')}}/${this.selectedNode}`)
.catch(console.error)
this.fetchedProducts = true;
this.products = response.data
this.loading = false;
},
getNodeInputText() {
if (this.fetchedNodes) {
if (this.nodes.length > 0) {
return '{{__('Please select a node...')}}';
}
return '{{__('No nodes found matching current configuration')}}'
}
return '{{__('---')}}';
},
getProductInputText() {
if (this.fetchedProducts) {
if (this.products.length > 0) {
return '{{__('Please select a resource...')}}';
}
return '{{__('No resources found matching current configuration')}}'
}
return '{{__('---')}}';
},
getEggInputText() {
if (this.selectedNest) {
return '{{__('Please select a configuration...')}}';
}
return '{{__('---')}}';
}
}
}
</script>
@endsection

View file

@ -16,6 +16,7 @@ use App\Http\Controllers\Admin\VoucherController;
use App\Http\Controllers\Auth\SocialiteController;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\NotificationController;
use App\Http\Controllers\ProductController as FrontProductController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\ServerController;
use App\Http\Controllers\StoreController;
@ -54,6 +55,12 @@ Route::middleware(['auth', 'checkSuspended'])->group(function () {
Route::resource('profile', ProfileController::class);
Route::resource('store', StoreController::class);
#server create utility routes (product)
#routes made for server create page to fetch product info
Route::get('/products/nodes/egg/{egg?}' , [FrontProductController::class , 'getNodesBasedOnEgg'])->name('products.nodes.egg');
Route::get('/products/products/node/{node?}' , [FrontProductController::class , 'getProductsBasedOnNode'])->name('products.products.node');
#payments
Route::get('checkout/{paypalProduct}', [PaymentController::class, 'checkOut'])->name('checkout');
Route::get('payment/success', [PaymentController::class, 'success'])->name('payment.success');