Add all files

This commit is contained in:
root
2025-11-30 11:57:22 +01:00
parent 2ee51cac4f
commit 044271db3d
1476 changed files with 142544 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Livewire\Auth;
use App\Livewire\Component;
use App\Models\User;
use App\Traits\Captchable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Session;
use Livewire\Attributes\Validate;
class Login extends Component
{
use Captchable;
#[Validate('required|email')]
public string $email = '';
#[Validate('required')]
public string $password = '';
public $remember = false;
public function submit()
{
$this->captcha();
$this->validate();
if (RateLimiter::tooManyAttempts('login:' . $this->email, 5)) {
$this->addError('email', 'Too many login attempts. Please try again in 60 seconds.');
return;
}
RateLimiter::increment('login:' . $this->email);
if (!Auth::attempt($this->only('email', 'password'), $this->remember)) {
$this->addError('email', 'These credentials do not match our records.');
return;
}
// Check 2FA
if (Auth::user()->tfa_secret) {
Session::put('2fa', [
'user_id' => Auth::id(),
'remember' => $this->remember,
'expires' => now()->addMinutes(5),
]);
Auth::logout();
return $this->redirect(route('2fa'), true);
}
RateLimiter::clear('login:' . $this->email);
event(new \App\Events\Auth\Login(User::find(Auth::id())));
$intendedUrl = session()->pull('url.intended', default: route('dashboard'));
$isAdminRoute = str_starts_with($intendedUrl, url('/admin'));
// Redirect normally if it is an admin route, otherwise navigate using livewire
return $this->redirect($intendedUrl, navigate: !$isAdminRoute);
}
public function render()
{
return view('auth.login');
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Livewire\Auth;
use App\Livewire\Component;
use Illuminate\Support\Facades\Auth;
class Logout extends Component
{
public function logout()
{
Auth::logout();
return redirect('/');
}
public function render()
{
return view('auth.logout');
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Livewire\Auth\Password;
use App\Helpers\NotificationHelper;
use App\Livewire\Component;
use App\Models\User;
use App\Traits\Captchable;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\RateLimiter;
class Request extends Component
{
use Captchable;
public string $email = '';
public function submit()
{
$this->captcha();
$this->validate([
'email' => 'required|email',
]);
$rateLimitKey = 'password-reset:' . request()->ip();
if (RateLimiter::tooManyAttempts($rateLimitKey, 3)) {
$this->addError('email', 'Too many password reset attempts. Please try again later.');
return;
}
RateLimiter::hit($rateLimitKey, 60);
// Find the user
$user = User::where('email', $this->email)->first();
if ($user && !$user?->role) {
NotificationHelper::passwordResetNotification($user, ['url' => url(route('password.reset', [
'token' => Password::createToken($user),
'email' => $user->email,
], false))]);
}
$this->notify('If the email address is associated with an account, you will receive an email with instructions on how to reset your password.', 'success');
}
public function render()
{
return view('auth.password.request');
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Livewire\Auth\Password;
use App\Livewire\Component;
use App\Models\User;
use App\Traits\Captchable;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Str;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Url;
use Livewire\Attributes\Validate;
class Reset extends Component
{
use Captchable;
public string $token;
#[Url, Locked]
public string $email = '';
#[Validate('required|string|min:8|confirmed')]
public string $password = '';
#[Validate('required')]
public string $password_confirmation = '';
public function mount()
{
if (!$this->token || !Request::has('email')) {
return abort(404);
}
// Validate token
if (!Password::tokenExists(User::where('email', $this->email)->firstOrFail(), $this->token)) {
return abort(404);
}
}
public function submit()
{
$this->validate();
$this->captcha();
$status = Password::reset(
['email' => $this->email, 'password' => $this->password, 'password_confirmation' => $this->password_confirmation, 'token' => $this->token],
function ($user) {
$user->forceFill([
'password' => Hash::make($this->password),
])->setRememberToken(Str::random(60));
$user->save();
event(new PasswordReset($user));
}
);
return $status === Password::PASSWORD_RESET ? $this->redirect(route('login')) : $this->notify($status, 'error');
}
public function render()
{
return view('auth.password.reset');
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Livewire\Auth;
use App\Attributes\DisabledIf;
use App\Livewire\ComponentWithProperties;
use App\Models\User;
use App\Traits\Captchable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
#[DisabledIf('registration_disabled')]
class Register extends ComponentWithProperties
{
use Captchable;
public string $first_name = '';
public string $last_name = '';
public string $email = '';
public string $password = '';
public string $password_confirmation = '';
public function mount()
{
$this->initializeProperties(null, User::class);
}
public function rules()
{
return array_merge([
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
], $this->getRulesForProperties());
}
public function submit()
{
$validatedData = $this->validate();
$user = User::create([
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'email' => $this->email,
'password' => Hash::make($this->password),
]);
if (array_key_exists('properties', $validatedData)) {
$this->updateProperties($user, $validatedData['properties']);
}
Auth::login($user);
return $this->redirectIntended(route('dashboard'), true);
}
public function render()
{
return view('auth.register');
}
}

74
app/Livewire/Auth/Tfa.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
namespace App\Livewire\Auth;
use App\Livewire\Component;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Session;
use RobThree\Auth\Providers\Qr\EndroidQrCodeProvider;
use RobThree\Auth\TwoFactorAuth;
class Tfa extends Component
{
public $code;
public function mount()
{
$session = Session::get('2fa');
if (!$session || $session['expires'] < now()) {
return $this->redirect(route('login'), true);
}
}
public function verify()
{
$this->validate([
'code' => 'required|numeric',
]);
if (RateLimiter::tooManyAttempts('2fa', 5)) {
$this->addError('code', 'Too many attempts.');
return;
}
RateLimiter::increment('2fa');
$session = Session::get('2fa');
$user = User::findOrfail($session['user_id']);
if (!$user->tfa_secret) {
return $this->redirect(route('login'), true);
}
$tfa = new TwoFactorAuth(new EndroidQrCodeProvider, config('app.name'));
if (!$tfa->verifyCode($user->tfa_secret, $this->code)) {
return $this->addError('code', 'Invalid code.');
}
// Check if code has been used before, preventing replay attacks
if (Cache::has('tfa_used_code_' . $user->id . '_' . $this->code)) {
return $this->addError('code', 'Invalid code.');
}
// Mark code as used for 60 seconds just in case
Cache::put('tfa_used_code_' . $user->id . '_' . $this->code, true, 60);
Auth::loginUsingId($user->id, $session['remember']);
event(new \App\Events\Auth\Login(User::find(Auth::id())));
Session::forget('2fa');
RateLimiter::clear('2fa');
return $this->redirect(route('dashboard'), true);
}
public function render()
{
return view('auth.2fa');
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Livewire\Auth;
use App\Helpers\NotificationHelper;
use App\Livewire\Component;
use App\Traits\Captchable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
class VerifyEmail extends Component
{
use Captchable;
public function mount()
{
if (Auth::user()->hasVerifiedEmail()) {
return redirect()->route('dashboard');
}
}
public function submit()
{
$this->captcha();
if (RateLimiter::tooManyAttempts('email-verification', 1)) {
$this->addError('code', 'Too many attempts. Try again later.');
return;
}
NotificationHelper::emailVerificationNotification(Auth::user());
RateLimiter::hit('email-verification', 120);
$this->notify('Verification email sent.');
}
public function render()
{
return view('auth.verify-email');
}
}

299
app/Livewire/Cart.php Normal file
View File

@@ -0,0 +1,299 @@
<?php
namespace App\Livewire;
use App\Classes\Cart as ClassesCart;
use App\Classes\Price;
use App\Exceptions\DisplayException;
use App\Helpers\ExtensionHelper;
use App\Jobs\Server\CreateJob;
use App\Models\Coupon;
use App\Models\Gateway;
use App\Models\Invoice;
use App\Models\Order;
use App\Models\Service;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Livewire\Attributes\Locked;
class Cart extends Component
{
#[Locked]
public $total;
#[Locked]
public array $gateways;
public $gateway;
public $coupon;
public $use_credits = true;
public $tos;
public function mount()
{
if (Session::has('coupon')) {
$this->coupon = Session::get('coupon');
}
$this->updateTotal();
}
private function updateTotal()
{
if (ClassesCart::get()->isEmpty()) {
$this->total = null;
return;
}
$this->total = new Price(['price' => ClassesCart::get()->sum(fn ($item) => $item->price->price * $item->quantity), 'currency' => ClassesCart::get()->first()->price->currency]);
$this->gateways = ExtensionHelper::getCheckoutGateways(ClassesCart::get(), 'cart');
if (count($this->gateways) > 0 && !array_search($this->gateway, array_column($this->gateways, 'id')) !== false) {
$this->gateway = $this->gateways[0]->id;
}
}
public function applyCoupon()
{
if ($this->coupon && Session::has('coupon')) {
return $this->notify('Coupon code already applied', 'error');
}
try {
ClassesCart::applyCoupon($this->coupon);
} catch (DisplayException $e) {
$this->notify($e->getMessage(), 'error');
$this->coupon = null;
return;
}
$this->coupon = Session::get('coupon');
$this->updateTotal();
$this->notify('Coupon code applied successfully', 'success');
}
public function removeCoupon()
{
if (!$this->coupon || !Session::has('coupon')) {
return $this->notify('No coupon code applied', 'error');
}
ClassesCart::removeCoupon();
$this->coupon = null;
$this->updateTotal();
$this->notify('Coupon code removed successfully', 'success');
}
public function removeProduct($index)
{
ClassesCart::remove($index);
$this->updateTotal();
}
public function updateQuantity($index, $quantity)
{
if (ClassesCart::get()->get($index)->product->allow_quantity !== 'combined') {
return;
}
if ($quantity < 1) {
$this->removeProduct($index);
return;
}
ClassesCart::get()->get($index)->quantity = $quantity;
session(['cart' => ClassesCart::get()->toArray()]);
$this->updateTotal();
}
// Checkout
public function checkout()
{
if (ClassesCart::get()->isEmpty() || Session::has('cart') === false) {
return $this->notify('Your cart is empty', 'error');
}
if (!Auth::check()) {
return redirect()->guest('login');
}
if (config('settings.mail_must_verify') && !Auth::user()->hasVerifiedEmail()) {
return redirect()->route('verification.notice');
}
if (config('settings.tos') && !$this->tos) {
return $this->notify('You must accept the terms of service', 'error');
}
// Re-validate coupon if one exists
if (Session::has('coupon') && !ClassesCart::validateAndRefreshCoupon()) {
$this->coupon = null;
$this->updateTotal();
return $this->notify('This coupon can no longer be used', 'error');
}
// Start database transaction
DB::beginTransaction();
try {
$user = User::where('id', Auth::id())->lockForUpdate()->first();
// Lock the orderproducts
foreach (ClassesCart::get() as $item) {
if (
$item->product->per_user_limit > 0 && ($user->services->where('product_id', $item->product->id)->count() >= $item->product->per_user_limit ||
ClassesCart::get()->filter(fn ($it) => $it->product->id == $item->product->id)->sum(fn ($it) => $it->quantity) + $user->services->where('product_id', $item->product->id)->count() > $item->product->per_user_limit
)
) {
throw new DisplayException(__('product.user_limit', ['product' => $item->product->name]));
}
if ($item->product->stock !== null) {
if ($item->product->stock < $item->quantity) {
throw new DisplayException(__('product.out_of_stock', ['product' => $item->product->name]));
}
$item->product->stock -= $item->quantity;
$item->product->save();
}
}
// Create the order
$order = new Order([
'user_id' => $user->id,
'currency_code' => $this->total->currency->code,
]);
$order->save();
// Create the invoice
if ($this->total->price > 0) {
$invoice = new Invoice([
'user_id' => $user->id,
'due_at' => now()->addDays(7),
'currency_code' => $this->total->currency->code,
]);
$invoice->save();
}
// Create the services
foreach (ClassesCart::get() as $item) {
// Is it a lifetime coupon, then we can adjust the price of the service
if ($this->coupon && $this->coupon->recurring != 1) {
$price = $item->price->price - $item->price->setup_fee;
} else {
$price = $item->price->original_price - $item->price->original_setup_fee;
}
// Create the service
$service = $order->services()->create([
'user_id' => $user->id,
'currency_code' => $this->total->currency->code,
'product_id' => $item->product->id,
'plan_id' => $item->plan->id,
'price' => $price,
'quantity' => $item->quantity,
'coupon_id' => Session::has('coupon') ? Session::get('coupon')->id : null,
]);
foreach ($item->checkoutConfig as $key => $value) {
$service->properties()->updateOrCreate([
'key' => $key,
], [
'value' => $value,
]);
}
foreach ($item->configOptions as $configOption) {
if (in_array($configOption->option_type, ['text', 'number', 'checkbox'])) {
if (!isset($configOption->value)) {
continue;
}
$service->properties()->updateOrCreate([
'key' => $configOption->option_env_variable ? $configOption->option_env_variable : $configOption->option_name,
], [
'name' => $configOption->option_name,
'value' => $configOption->value,
]);
continue;
}
$service->configs()->create([
'config_option_id' => $configOption->option_id,
'config_value_id' => $configOption->value,
]);
}
// Create the invoice items
if ($item->price->price > 0) {
$invoice->items()->create([
'reference_id' => $service->id,
'reference_type' => Service::class,
'price' => $item->price->price,
'quantity' => $item->quantity,
'description' => $service->description,
]);
} else {
// We'll make the service active immediately
if ($service->product->server) {
CreateJob::dispatch($service);
}
$service->status = Service::STATUS_ACTIVE;
$service->expires_at = $service->calculateNextDueDate();
$service->save();
}
}
// We don't wanna use credits if the total price is 0, duh
if ($this->use_credits && $this->total->price > 0) {
$credit = Auth::user()->credits()->where('currency_code', $this->total->currency->code)->first();
if ($credit && $credit->amount > 0) {
// Is it more credits or less credits than the total price?
if ($credit->amount >= $this->total->price) {
$credit->amount -= $this->total->price;
$credit->save();
ExtensionHelper::addPayment($invoice->id, null, amount: $this->total->price);
} else {
$this->total->price -= $credit->amount;
ExtensionHelper::addPayment($invoice->id, null, amount: $credit->amount);
$credit->amount = 0;
$credit->save();
}
}
}
// Commit the transaction
DB::commit();
// Clear the cart
Session::forget('cart');
Session::forget('coupon');
// Pass the gateway to the payment page
Session::put(['gateway' => $this->gateway]);
if ($this->total->price == 0) {
// Is it only one item? Then redirect to the service page
if ($order->services->count() == 1) {
return $this->redirect(route('services.show', $order->services->first()), true);
}
return $this->redirect(route('services'), true);
} else {
return $this->redirect(route('invoices.show', $invoice) . '?pay');
}
} catch (\Exception $e) {
// Rollback the transaction
DB::rollBack();
// Return error message
// Is it a real error or a validation error?
// If it's a validation error, you can use the $this->addError() method to display the error message to the user.
if ($e instanceof DisplayException) {
return $this->notify($e->getMessage(), 'error');
} else {
Log::error($e);
$this->notify('An error occurred while processing your order. Please try again later.');
}
throw $e;
}
}
public function render()
{
return view('cart');
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Livewire\Client;
use App\Livewire\ComponentWithProperties;
use Illuminate\Support\Facades\Auth;
class Account extends ComponentWithProperties
{
public string $first_name = '';
public string $last_name = '';
public string $email = '';
public function mount()
{
$user = Auth::user();
$this->first_name = $user->first_name;
$this->last_name = $user->last_name;
$this->email = $user->email;
$this->initializeProperties($user, $user::class);
}
public function rules()
{
return array_merge([
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'email' => 'required|email|max:255|unique:users,email,' . Auth::id(),
], $this->getRulesForProperties());
}
public function validationAttributes()
{
return $this->getAttributesForProperties();
}
public function submit()
{
$validatedData = $this->validate();
/** @var \App\Models\User $user */
$user = Auth::user();
$user->update($validatedData);
if (array_key_exists('properties', $validatedData)) {
$this->updateProperties($user, $validatedData['properties']);
}
$this->notify(__('Account updated successfully.'));
}
public function render()
{
return view('client.account.index')->layoutData([
'sidebar' => true,
]);
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace App\Livewire\Client;
use App\Exceptions\DisplayException;
use App\Helpers\ExtensionHelper;
use App\Livewire\Component;
use App\Models\Credit;
use App\Models\Gateway;
use App\Models\Invoice;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
class Credits extends Component
{
#[Validate('required|exists:currencies,code')]
public $currency;
public $amount;
#[Locked]
public $gateways;
public $gateway;
public function mount()
{
if (!config('settings.credits_enabled')) {
return redirect()->route('account');
}
$this->amount = config('settings.credits_minimum_deposit');
$this->currency = session('currency', config('settings.default_currency'));
$this->gateways = ExtensionHelper::getCheckoutGateways([], 'credits');
if (count($this->gateways) > 0 && !array_search($this->gateway, array_column($this->gateways, 'id')) !== false) {
$this->gateway = $this->gateways[0]->id;
}
}
public function addCredit()
{
$this->validate([
'currency' => 'required|exists:currencies,code',
'amount' => 'required|numeric|min:' . config('settings.credits_minimum_deposit') . '|max:' . config('settings.credits_maximum_deposit'),
'gateway' => 'required|in:' . implode(',', array_column($this->gateways, 'id')),
]);
if (Auth::user()->credits()->where('currency_code', $this->currency)->exists()) {
// Check if the current credits + the new credits exceed the maximum credits allowed
if (Auth::user()->credits()->where('currency_code', $this->currency)->sum('amount') + $this->amount > config('settings.credits_maximum_credit')) {
$this->notify('You cannot exceed the maximum credits allowed.', 'error');
return;
}
}
// Create invoice
DB::beginTransaction();
try {
$invoice = Invoice::create([
'user_id' => Auth::id(),
'currency_code' => $this->currency,
'due_at' => now(),
]);
$invoice->items()->create([
'description' => __('account.credit_deposit', ['currency' => $this->currency]),
'quantity' => 1,
'price' => $this->amount,
'reference_type' => Credit::class,
]);
DB::commit();
Session::put(['gateway' => $this->gateway]);
// Redirect to the invoices page and pay the invoice
if ($this->gateway) {
$pay = ExtensionHelper::pay(Gateway::where('id', $this->gateway)->first(), $invoice->fresh());
if (is_string($pay)) {
return $this->redirect($pay);
}
}
return $this->redirect(route('invoices.show', $invoice) . '?gateway=' . $this->gateway . '&pay', true);
} catch (\Exception $e) {
// Rollback the transaction
DB::rollBack();
// Return error message
// Is it a real error or a validation error?
// If it's a validation error, you can use the $this->addError() method to display the error message to the user.
if ($e instanceof DisplayException) {
$this->notify($e->getMessage(), 'error');
} else {
Log::error($e);
$this->notify('An error occurred while processing your order. Please try again later.');
}
throw $e;
}
}
public function render()
{
return view('client.account.credits')->layoutData([
'sidebar' => true,
]);
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace App\Livewire\Client;
use App\Livewire\Component;
use App\Models\Session;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Locked;
use RobThree\Auth\Providers\Qr\EndroidQrCodeProvider;
use RobThree\Auth\TwoFactorAuth;
class Security extends Component
{
public string $current_password = '';
public string $password = '';
public string $password_confirmation = '';
public bool $twoFactorEnabled = false;
#[Locked]
public $twoFactorData = [];
#[Locked]
public $showEnableTwoFactor = false;
public string $twoFactorCode = '';
public function mount()
{
$this->twoFactorEnabled = Auth::user()->tfa_secret ? true : false;
}
public function rules()
{
return [
'current_password' => 'required|string',
'password' => 'required|string|min:8|confirmed',
];
}
public function changePassword()
{
$this->validate();
if (!Hash::check($this->current_password, Auth::user()->password)) {
return $this->notify(__('account.notifications.password_incorrect'), 'error');
}
Auth::user()->update([
'password' => Hash::make($this->password),
]);
$this->notify(__('account.notifications.password_changed'));
$this->reset('current_password', 'password', 'password_confirmation');
}
public function enableTwoFactor()
{
if ($this->showEnableTwoFactor) {
$this->validate([
'twoFactorCode' => 'required|string',
]);
$tfa = new TwoFactorAuth(new EndroidQrCodeProvider, config('app.name'));
if ($tfa->verifyCode($this->twoFactorData['secret'], $this->twoFactorCode)) {
Auth::user()->update([
'tfa_secret' => $this->twoFactorData['secret'],
]);
$this->notify(__('account.notifications.two_factor_enabled'));
$this->twoFactorEnabled = true;
$this->showEnableTwoFactor = false;
} else {
$this->notify(__('account.notifications.two_factor_code_incorrect'), 'error');
}
} else {
$tfa = new TwoFactorAuth(new EndroidQrCodeProvider, config('app.name'));
$secret = $tfa->createSecret();
$this->twoFactorData = [
'secret' => $secret,
'image' => $tfa->getQRCodeImageAsDataUri(Auth::user()->email, $secret),
];
$this->showEnableTwoFactor = true;
}
}
public function disableTwoFactor()
{
Auth::user()->update([
'tfa_secret' => null,
]);
$this->notify(__('account.notifications.two_factor_disabled'));
$this->twoFactorEnabled = false;
}
public function logoutSession(Session $session)
{
$session->delete();
$this->notify(__('account.notifications.session_logged_out'));
}
public function render()
{
return view('client.account.security')->layoutData([
'sidebar' => true,
]);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Livewire;
use App\Exceptions\DisplayException;
class Component extends \Livewire\Component
{
use Traits\Disabled, Traits\HasNotifications;
public function paginationView()
{
return 'components.pagination';
}
public function exception($e, $stopPropagation)
{
if ($e instanceof DisplayException) {
$this->notify($e->getMessage(), 'error');
$stopPropagation();
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace App\Livewire;
use App\Events\Properties\Updated as PropertiesUpdated;
use Illuminate\Database\Eloquent\Collection;
class ComponentWithProperties extends Component
{
// Custom Properties which will be used to render the Inputs
public Collection $custom_properties;
// Values of the Custom Properties
public array $properties = [];
/**
* Updates the properties of the model
*
* @param \Illuminate\Database\Eloquent\Model|null $model
* @param string $morphClass
*/
public function initializeProperties($model, $morphClass)
{
$this->custom_properties = \App\Models\CustomProperty::where('model', $morphClass)->get();
if ($model) {
$this->properties = $model
->properties->mapWithKeys(function ($property) {
return [$property->key => $property->value];
})
->toArray();
}
}
public function getRulesForProperties(): array
{
return $this->custom_properties->mapWithKeys(function ($property) {
return ["properties.$property->key" => ($property->required ? 'required|' : 'nullable|') . "$property->validation"];
})->toArray();
}
/**
* Returns the attributes of the properties to show in error messages
*/
public function getAttributesForProperties(): array
{
return $this->custom_properties->mapWithKeys(function ($property) {
return ["properties.$property->key" => $property->name];
})->toArray();
}
/**
* Updates the properties of the model
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param array $properties
*/
public function updateProperties($model, $properties)
{
$properties = collect($properties)->map(function ($value, $key) use ($model) {
$custom_property = $this->custom_properties->where('key', $key)->first();
return [
'key' => $key,
'value' => $value,
'model_id' => $model->id,
'model_type' => $model->getMorphClass(),
'name' => $custom_property->name,
'custom_property_id' => $custom_property->id,
];
})->toArray();
$model->properties()->upsert($properties, uniqueBy: [
'key',
'model_id',
'model_type',
], update: [
'name',
'value',
'model_id',
'model_type',
'custom_property_id',
]);
event(new PropertiesUpdated($model, $properties));
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Livewire\Components;
use App\Classes\Cart as ClassesCart;
use App\Livewire\Component;
use Livewire\Attributes\On;
class Cart extends Component
{
public $cartCount;
public function mount()
{
$this->cartCount = ClassesCart::get()->count();
if (ClassesCart::get()->isEmpty()) {
$this->skipRender();
}
}
#[On('cartUpdated')]
public function onCartUpdated()
{
$this->cartCount = ClassesCart::get()->count();
}
public function render()
{
return view('components.cart');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Livewire\Components;
use App\Classes\Cart;
use App\Livewire\Component;
use App\Models\Currency;
class CurrencySwitch extends Component
{
public $currentCurrency;
protected $currencies = [];
public function mount()
{
$this->currentCurrency = session('currency', config('settings.default_currency'));
$this->currencies = Currency::all()->map(fn ($currency) => [
'value' => $currency->code,
'label' => $currency->code,
])->values()->toArray();
if (Cart::get()->isNotEmpty() || count($this->currencies) <= 1) {
$this->skipRender();
}
}
public function updatedCurrentCurrency($currency)
{
if (Cart::get()->isNotEmpty()) {
$this->notify('You cannot change the currency while there are items in the cart.', 'error');
$this->currentCurrency = session('currency', config('settings.default_currency'));
return;
}
session(['currency' => $currency]);
return $this->redirect(request()->header('Referer', '/'), navigate: true);
}
public function render()
{
return view('components.currency-switch');
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Livewire\Components;
use App\Livewire\Component;
class LanguageSwitch extends Component
{
public $currentLocale;
public function mount()
{
$this->currentLocale = session('locale', config('app.locale'));
}
public function updatedCurrentLocale($locale)
{
if (!array_key_exists($locale, config('app.available_locales', []))) {
return;
}
session(['locale' => $locale]);
app()->setLocale($locale);
return $this->redirect(request()->header('Referer', '/'), navigate: true);
}
public function render()
{
$locales = config('app.available_locales');
return view('components.language-switch', compact('locales'));
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Livewire;
class Dashboard extends Component
{
public $activeComponent = 'services';
public function render()
{
return view('dashboard')->layoutData([
'sidebar' => true,
]);
}
}

21
app/Livewire/Home.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
namespace App\Livewire;
use App\Models\Category;
class Home extends Component
{
public function render()
{
return view('home', [
// Categories which have NO parent and at least one child
'categories' => Category::whereNull('parent_id')->where(function ($query) {
$query->whereHas('children')->orWhereHas('products', function ($query) {
$query->where('hidden', false);
});
})->orderBy('sort')->get(),
'title' => 'Home',
]);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Livewire\Invoices;
use App\Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Livewire\WithPagination;
class Index extends Component
{
use WithPagination;
public function render()
{
return view('invoices.index', [
'invoices' => Auth::user()->invoices()->orderBy('id', 'desc')->paginate(config('settings.pagination')),
])->layoutData([
'title' => __('invoices.invoices'),
'sidebar' => true,
]);
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace App\Livewire\Invoices;
use App\Classes\PDF;
use App\Helpers\ExtensionHelper;
use App\Livewire\Component;
use App\Models\Credit;
use App\Models\Gateway;
use App\Models\Invoice;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Request;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Url;
class Show extends Component
{
#[Locked]
public Invoice $invoice;
#[Url]
public $gateway = null;
#[Locked]
public $gateways;
public $checkPayment = false;
private $pay = null;
public $use_credits = false;
public function mount()
{
$sessionGateway = session('gateway');
if ($sessionGateway) {
$this->gateway = $sessionGateway;
}
if ($this->invoice->status === 'pending') {
$this->gateways = ExtensionHelper::getCheckoutGateways($this->invoice->items, 'invoice');
if (count($this->gateways) > 0 && !array_search($this->gateway, array_column($this->gateways, 'id')) !== false) {
$this->gateway = $this->gateways[0]->id;
}
if ($sessionGateway && Request::has('pay')) {
$this->pay();
}
}
if (Request::has('checkPayment') && $this->invoice->status === 'pending') {
$this->checkPayment = true;
}
// We don't want to toggle use_credits before $this->pay() is called, otherwise it will always paid with credits
$this->use_credits = true;
$hasCredits = $this->invoice->items()->where('reference_type', Credit::class)->exists();
if ($hasCredits) {
$this->use_credits = false;
}
}
public function pay()
{
if ($this->use_credits) {
$credit = Auth::user()->credits()->where('currency_code', $this->invoice->currency_code)->first();
if ($credit && $credit->amount > 0) {
// Is it more credits or less credits than the total price?
if ($credit->amount >= $this->invoice->remaining) {
$credit->amount -= $this->invoice->remaining;
$credit->save();
ExtensionHelper::addPayment($this->invoice->id, null, amount: $this->invoice->remaining);
return $this->redirect(route('invoices.show', $this->invoice), true);
} else {
ExtensionHelper::addPayment($this->invoice->id, null, amount: $credit->amount);
$credit->amount = 0;
$credit->save();
$this->invoice = $this->invoice->fresh();
}
}
}
if ($this->invoice->status !== 'pending') {
return $this->notify(__('This invoice cannot be paid.'), 'error');
}
if ($this->checkPayment) {
$this->checkPayment = false;
}
$this->validate([
'gateway' => 'required',
]);
$this->pay = ExtensionHelper::pay(Gateway::where('id', $this->gateway)->first(), $this->invoice);
if (is_string($this->pay)) {
$this->redirect($this->pay);
}
}
public function exitPay()
{
$this->pay = null;
// Dispatch event so extensions can do their thing
$this->dispatch('invoice.payment.cancelled', $this->invoice);
// Refresh invoice status
$this->redirect(route('invoices.show', $this->invoice), true);
}
public function checkPaymentStatus()
{
$this->invoice->refresh();
if ($this->invoice->status === 'paid') {
$this->notify(__('The invoice has been paid.'), 'success');
$this->checkPayment = false;
}
}
public function render()
{
return view('invoices.show')->layoutData([
'title' => __('invoices.invoice', ['id' => $this->invoice->number]),
'sidebar' => true,
]);
}
public function downloadPDF()
{
return response()->streamDownload(function () {
echo PDF::generateInvoice($this->invoice)->stream();
}, 'invoice-' . $this->invoice->number . '.pdf');
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Livewire\Invoices;
use App\Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Livewire\WithPagination;
class Widget extends Component
{
use WithPagination;
public function render()
{
return view('invoices.widget', [
'invoices' => Auth::user()->invoices()->orderBy('id', 'desc')->where('status', '=', 'pending')->paginate(config('settings.pagination')),
])->layoutData([
'title' => __('invoices.invoices'),
]);
}
}

View File

@@ -0,0 +1,242 @@
<?php
namespace App\Livewire\Products;
use App\Classes\Cart;
use App\Classes\Price;
use App\Helpers\ExtensionHelper;
use App\Livewire\Component;
use App\Models\Category;
use App\Models\Plan;
use App\Models\Price as ModelsPrice;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Url;
class Checkout extends Component
{
public $product;
public Category $category;
public Plan $plan;
#[Url(keep: true, as: 'plan')]
public $plan_id;
// Don't allow the user to change the total via hacks
#[Locked]
public $total;
public $setup_fee;
#[Url(keep: true, as: 'options')]
public $configOptions = [];
#[Url(keep: true, as: 'config')]
public $checkoutConfig = [];
#[Url(as: 'edit'), Locked]
public $cartProductKey = null;
public function mount($product)
{
$this->product = $this->category->products()->where('slug', $product)->firstOrFail();
if ($this->product->stock === 0) {
return $this->redirect(route('products.show', ['category' => $this->category, 'product' => $this->product]), true);
}
// Is there a existing item in the cart?
if (Cart::get()->has($this->cartProductKey) && Cart::get()->get($this->cartProductKey)->product->id === $this->product->id) {
$item = Cart::get()->get($this->cartProductKey);
// Get the item from the cart
$this->plan = $item->plan->fresh();
$this->plan_id = $this->plan->id;
$this->configOptions = $item->configOptions->mapWithKeys(function ($option) {
return [$option->option_id => $option->value];
});
$this->checkoutConfig = (array) $item->checkoutConfig;
} else {
// Set the first plan as default
$this->plan = $this->plan_id ? $this->product->plans->findOrFail($this->plan_id) : $this->product->plans->first();
$this->plan_id = $this->plan->id;
// Prepare the config options
$this->configOptions = $this->product->configOptions->mapWithKeys(function ($option) {
if (in_array($option->type, ['text', 'number', 'checkbox'])) {
return [$option->id => $this->configOptions[$option->id] ?? null];
}
return [$option->id => $this->configOptions[$option->id] ?? $option->children->first()->id];
})->toArray();
foreach ($this->getCheckoutConfig() as $config) {
if (in_array($config['type'], ['select', 'radio'])) {
$this->checkoutConfig[$config['name']] = $this->checkoutConfig[$config['name']] ?? $config['default'] ?? array_key_first($config['options']);
} else {
$this->checkoutConfig[$config['name']] = $this->checkoutConfig[$config['name']] ?? $config['default'] ?? null;
}
}
}
// Update the pricing
$this->updatePricing();
// As there is only one plan, config options and checkout config, we can directly call the checkout method to avoid confusion
// This is only done when the user is not editing the cart item
if ($this->product->plans->count() === 1 && empty($this->configOptions) && empty($this->checkoutConfig)) {
$this->checkout();
}
}
public function updatePricing()
{
$total = $this->plan->price()->price + $this->product->configOptions->sum(function ($option) {
if ($option->type === 'checkbox' && (isset($this->configOptions[$option->id]) && $this->configOptions[$option->id])) {
return $option->children->first()?->price(billing_period: $this->plan->billing_period, billing_unit: $this->plan->billing_unit)->price;
}
if (in_array($option->type, ['text', 'number', 'checkbox'])) {
return 0;
}
return $option->children->where('id', $this->configOptions[$option->id])->first()?->price(billing_period: $this->plan->billing_period, billing_unit: $this->plan->billing_unit)->price;
});
$setup_fee = $this->plan->price()->setup_fee + $this->product->configOptions->sum(function ($option) {
if (in_array($option->type, ['text', 'number', 'checkbox'])) {
return $option->children->first()?->price(billing_period: $this->plan->billing_period, billing_unit: $this->plan->billing_unit)->setup_fee;
}
return $option->children->where('id', $this->configOptions[$option->id])->first()?->price(billing_period: $this->plan->billing_period, billing_unit: $this->plan->billing_unit)->setup_fee;
});
$this->total = new Price([
'price' => $total + $setup_fee,
'currency' => $this->plan->price()->currency,
'setup_fee' => $setup_fee,
], apply_exclusive_tax: true);
}
// On change of the plan, update the config options
public function updatedPlanId($value)
{
$this->plan = Plan::findOrFail($value);
$this->updatePricing();
}
// On change of the config options, update the pricing
public function updatedConfigOptions()
{
$this->updatePricing();
}
public function getCheckoutConfig()
{
return once(fn () => ExtensionHelper::getCheckoutConfig($this->product, $this->checkoutConfig));
}
public function rules()
{
$rules = [
'plan_id' => [
'required',
Rule::exists('plans', 'id')->where(function ($query) {
$query->where('priceable_id', $this->product->id)->where('priceable_type', get_class($this->product));
}),
],
];
foreach ($this->product->configOptions as $option) {
if (in_array($option->type, ['text', 'number'])) {
$rules["configOptions.{$option->id}"] = ['required'];
} elseif ($option->type === 'checkbox') {
} else {
$rules["configOptions.{$option->id}"] = ['required', 'exists:config_options,id'];
}
}
foreach ($this->getCheckoutConfig() as $key => $config) {
$validationRules = [];
if ($config['required'] ?? false) {
$validationRules[] = 'required';
}
if (isset($config['type'])) {
switch ($config['type']) {
case 'text':
case 'number':
$validationRules[] = 'string';
break;
case 'select':
case 'radio':
$validationRules[] = 'in:' . implode(',', array_keys($config['options']));
break;
case 'checkbox':
$validationRules[] = 'nullable';
$validationRules[] = 'boolean';
break;
}
}
if (isset($config['validation'])) {
if (is_array($config['validation'])) {
$validationRules = array_merge($validationRules, $config['validation']);
} else {
// Is validation seperated by |?
$validationRules = array_merge($validationRules, explode('|', $config['validation']));
}
}
if (count($validationRules) > 0) {
$rules["checkoutConfig.{$config['name']}"] = $validationRules;
}
}
return $rules;
}
public function attributes()
{
$messages = [];
foreach ($this->product->configOptions as $option) {
$messages["configOptions.{$option->id}"] = $option->name;
}
foreach ($this->getCheckoutConfig() as $key => $config) {
$messages["checkoutConfig.{$config['name']}"] = $config['label'] ?? $config['name'];
}
return $messages;
}
public function checkout()
{
// Do the checkout
// First we validate the plans
$this->validate(attributes: $this->attributes());
// Change configOptions so they also contain the name of the option (resulting in less database calls = faster speeds)
$configOptions = $this->product->configOptions->map(function ($option) {
if (in_array($option->type, ['text', 'number', 'checkbox'])) {
return (object) ['option_id' => $option->id, 'option_name' => $option->name, 'option_type' => $option->type, 'option_env_variable' => $option->env_variable, 'value' => $this->configOptions[$option->id], 'value_name' => $this->configOptions[$option->id]];
}
return (object) ['option_id' => $option->id, 'option_name' => $option->name, 'option_type' => $option->type, 'option_env_variable' => $option->env_variable, 'value' => $this->configOptions[$option->id], 'value_name' => $option->children->where('id', $this->configOptions[$option->id])->first()->name];
});
Cart::add($this->product, $this->plan, $configOptions, $this->checkoutConfig, $this->total, key: $this->cartProductKey);
$this->dispatch('cartUpdated');
return $this->redirect(route('cart'), true);
}
private function total()
{
// A class isn't allowed to be a Livewire property
$totalPrice['price'] = new ModelsPrice(['price' => $this->total]);
$totalPrice['currency'] = $this->plan->price()->currency;
$totalPrice = new Price((object) $totalPrice);
return $totalPrice;
}
public function render()
{
return view('products.checkout')->layoutData([
'title' => $this->product->name,
'image' => $this->product->image ? Storage::url($this->product->image) : null,
]);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Livewire\Products;
use App\Livewire\Component;
use App\Models\Category;
use Illuminate\Support\Facades\Storage;
class Index extends Component
{
public $products;
public $categories;
public $childCategories;
public Category $category;
public function mount()
{
$this->products = $this->category->products()->where('hidden', false)->with('category')->orderBy('sort')->get();
$this->childCategories = $this->category->children()->where(function ($query) {
$query->whereHas('children')->orWhereHas('products', function ($query) {
$query->where('hidden', false);
});
})->orderBy('sort')->get();
$this->categories = Category::whereNull('parent_id')->where(function ($query) {
$query->whereHas('children')->orWhereHas('products', function ($query) {
$query->where('hidden', false);
});
})->orderBy('sort')->get();
if (count($this->products) === 0 && count($this->childCategories) === 0) {
abort(404);
}
}
public function render()
{
return view('products.index')->layoutData([
'title' => $this->category->name,
'image' => $this->category->image ? Storage::url($this->category->image) : null,
]);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Livewire\Products;
use App\Livewire\Component;
use App\Models\Category;
use Illuminate\Support\Facades\Storage;
class Show extends Component
{
public $product;
public Category $category;
public function mount($product)
{
$this->product = $this->category->products()->where('slug', $product)->where('hidden', false)->firstOrFail();
}
public function render()
{
return view('products.show')->layoutData([
'title' => $this->product->name,
'image' => $this->product->image ? Storage::url($this->product->image) : null,
]);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Livewire\Services;
use App\Livewire\Component;
use App\Models\Service;
use App\Models\ServiceCancellation;
use Livewire\Attributes\Validate;
class Cancel extends Component
{
public Service $service;
#[Validate('required|in:end_of_period,immediate')]
public $type = 'end_of_period';
#[Validate('required')]
public $reason = '';
public function cancelService()
{
$this->authorize('view', $this->service);
$this->validate();
// Event hook will handle the cancellation (if its immediate or end of period)
ServiceCancellation::create([
'service_id' => $this->service->id,
'type' => $this->type,
'reason' => $this->reason,
]);
$this->notify(__('services.cancellation_requested'), 'success');
$this->redirect(route('services.show', $this->service), true);
}
public function render()
{
return view('services.cancel')->layoutData([
'title' => __('services.cancellation', ['service' => $this->service->product->name]),
'sidebar' => true,
]);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Livewire\Services;
use App\Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Livewire\WithPagination;
class Index extends Component
{
use WithPagination;
public $status = null;
public function render()
{
$query = Auth::user()->services()
->with([
'product.category',
'product.server',
'plan',
'proxmoxServer',
])
->orderBy('created_at', 'desc');
if ($this->status) {
$query->where('status', $this->status);
}
return view('services.index', [
'services' => $query->paginate(config('settings.pagination')),
])->layoutData([
'title' => 'Services',
'sidebar' => true,
]);
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace App\Livewire\Services;
use App\Helpers\ExtensionHelper;
use App\Livewire\Component;
use App\Models\Service;
use Paymenter\Extensions\Servers\Proxmox\Models\Server as ProxmoxServer;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Url;
class Show extends Component
{
public Service $service;
public ?ProxmoxServer $proxmoxServer = null;
#[Locked]
public $buttons = [];
#[Locked]
public $views = [];
#[Url('tab', except: false), Locked]
public $currentView;
#[Url('cancel', except: false)]
public bool $showCancel = false;
public function mount()
{
// Attach Proxmox server (if any) so we can show hostname on the service page
$this->proxmoxServer = ProxmoxServer::where('service_id', $this->service->id)->first();
// Only fetch the actions if the service is active
if ($this->service->status == Service::STATUS_ACTIVE) {
$actions = [];
try {
$actions = ExtensionHelper::getActions($this->service);
} catch (\Exception $e) {
}
// separate the actions into buttons and views
foreach ($actions as $action) {
if ($action['type'] == 'button') {
$this->buttons[] = $action;
} elseif ($action['type'] == 'view') {
$this->views[] = $action;
}
}
$this->currentView = $this->currentView ?? ($this->views[0]['name'] ?? null);
}
}
public function changeView($view)
{
if (!$view) {
return;
}
if ($this->currentView === $view || !in_array($view, array_column($this->views, 'name'))) {
return $this->skipRender();
}
$this->currentView = $view;
}
public function updatedShowCancel($value)
{
if (!$this->service->cancellable) {
$this->notify('This service cannot be cancelled', 'error');
$this->showCancel = false;
return;
}
}
public function goto($function)
{
// Check if function is allowed
if (!in_array($function, array_column($this->buttons, 'function'))) {
$this->notify('This action is not allowed', 'error');
return;
}
$result = ExtensionHelper::callService($this->service, $function);
// If its a response, return it
if (!is_string($result)) {
return $result;
}
$this->redirect($result);
}
public function render()
{
$view = null;
$previousView = $this->currentView;
if ($this->currentView) {
try {
// Search array for the current view
$currentViewObj = $this->views[array_search($this->currentView, array_column($this->views, 'name'))] ?? null;
if (!$currentViewObj) {
throw new \Exception('View not found');
}
$view = ExtensionHelper::getView($this->service, $currentViewObj);
} catch (\Exception $e) {
if ($previousView !== $this->views[0]['name'] ?? null) {
$this->notify('Got an error while trying to load the view', 'error');
}
$this->currentView = $this->views[0]['name'] ?? null;
}
}
return view('services.show', ['extensionView' => $view])->layoutData([
'title' => 'Services',
'sidebar' => true,
]);
}
}

View File

@@ -0,0 +1,263 @@
<?php
namespace App\Livewire\Services;
use App\Classes\Price;
use App\Events\Invoice\Created as InvoiceCreated;
use App\Jobs\Server\UpgradeJob;
use App\Livewire\Component;
use App\Models\Invoice;
use App\Models\Product;
use App\Models\Service;
use App\Models\ServiceUpgrade;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
class Upgrade extends Component
{
public Service $service;
public $upgrade;
public Product $upgradeProduct;
public int $step = 1;
public $configOptions = [];
public function mount()
{
$this->authorize('view', $this->service);
if (!$this->service->upgradable) {
$this->notify('This service is not upgradable.', 'error');
return $this->redirect(route('services.show', $this->service), true);
}
$this->upgradeProduct = $this->service->product;
$this->upgrade = $this->service->product->id;
$this->totalToday();
// We only have upgrabble config options if the product has any
if ($this->service->productUpgrades()->count() === 0) {
$this->nextStep();
}
}
#[Computed]
public function totalToday()
{
$upgrade = new ServiceUpgrade([
'service' => $this->service,
'product' => $this->upgradeProduct,
]);
$total = $upgrade->calculateProratedAmount(
$this->service->product,
$this->upgradeProduct
)->price;
// Calculate prices for config options
foreach ($this->configOptions as $optionId => $value) {
$option = $this->upgradeProduct->upgradableConfigOptions->where('id', $optionId)->first();
if (!$option || !$option->children->contains('id', $value)) {
continue;
}
$oldPrice = $this->service->configs->where('config_option_id', $optionId)->first();
$ctotal = $upgrade->calculateProratedAmount(
$oldPrice ? $oldPrice->configValue : null,
$option->children->find($value)
);
$total += $ctotal->price;
}
return new Price([
'price' => $total,
'currency' => $this->service->currency,
]);
}
// When upgrade changes, update the upgradeProduct
public function updatedUpgrade($upgrade)
{
// Check if the upgrade is valid
if (!$this->service->productUpgrades()->contains($upgrade) && $upgrade != $this->service->product_id) {
$this->notify('Invalid upgrade.', 'error');
return;
}
$this->upgradeProduct = Product::findOrFail($upgrade);
}
public function nextStep()
{
$currentConfigOptions = $this->service->configs->pluck('config_value_id', 'config_option_id')->toArray();
$this->configOptions = $this->upgradeProduct->upgradableConfigOptions->mapWithKeys(function ($option) use ($currentConfigOptions) {
return [
$option->id => $currentConfigOptions[$option->id]
?? $this->configOptions[$option->id]
?? $option->children->first()->id,
];
})->toArray();
$this->step++;
}
public function rules()
{
$rules = [
'upgradeProduct.id' => [
'required',
function ($attribute, $value, $fail) {
$plan = $this->upgradeProduct->availablePlans()
->where('billing_period', $this->service->plan->billing_period)
->where('billing_unit', $this->service->plan->billing_unit)
->first();
if (!$plan) {
$fail(__('Invalid upgrade.'));
}
},
],
];
foreach ($this->upgradeProduct->upgradableConfigOptions as $option) {
if (in_array($option->type, ['text', 'number'])) {
$rules["configOptions.{$option->id}"] = ['required'];
} elseif ($option->type === 'checkbox') {
} else {
$rules["configOptions.{$option->id}"] = ['required', 'exists:config_options,id'];
}
}
return $rules;
}
public function doUpgrade()
{
if (!$this->service->upgradable) {
$this->notify('This service is not upgradable.', 'error');
return $this->redirect(route('services.show', $this->service), true);
}
$this->validate();
$upgradePlan = $this->upgradeProduct->availablePlans()->where('billing_period', $this->service->plan->billing_period)->where('billing_unit', $this->service->plan->billing_unit)->first();
// The old config options must be in upgradableConfigOptions
$serviceConfigOptions = $this->service->configs->pluck('config_value_id', 'config_option_id')->toArray();
$configOptions = collect($serviceConfigOptions)->filter(function ($value, $key) use ($serviceConfigOptions) {
return isset($serviceConfigOptions[$key]) && $this->upgradeProduct->upgradableConfigOptions->contains('id', $key);
})->toArray();
// If the product did not change and the config options are the same, present the user with a message
if ($this->upgradeProduct->id === $this->service->product_id && $this->configOptions == $configOptions) {
$this->notify('You have not changed anything. Please select a different product or change the configuration options.', 'error');
return;
}
$upgrade = new ServiceUpgrade([
'service_id' => $this->service->id,
'product_id' => $this->upgradeProduct->id,
'plan_id' => $upgradePlan->id,
]);
$upgrade->save();
if ($this->configOptions) {
foreach ($this->configOptions as $optionId => $value) {
$upgrade->configs()->create([
'config_option_id' => $optionId,
'config_value_id' => $value,
]);
}
}
$price = $upgrade->calculatePrice();
if ($price->price <= 0) {
$upgrade->status = ServiceUpgrade::STATUS_COMPLETED;
$upgrade->save();
$upgrade->service()->update([
'plan_id' => $upgrade->plan_id,
'product_id' => $upgrade->product_id,
]);
// Update the service configs
foreach ($upgrade->configs as $config) {
$upgrade->service->configs()->updateOrCreate(
['config_option_id' => $config->config_option_id],
['config_value_id' => $config->config_value_id]
);
}
$this->service->refresh();
$this->service->recalculatePrice();
if ($this->service->product->server) {
// If the service has a server, dispatch the upgrade job
UpgradeJob::dispatch($this->service);
}
// Check if user has credits in this currency
/** @var \App\Models\User */
$user = Auth::user();
$credit = $user->credits()->where('currency_code', $price->currency->code)->first();
if ($credit) {
// Increment the credits, `abs()` ensures the amount to add is positive
$credit->increment('amount', abs($price->price));
} else {
$user->credits()->create([
'currency_code' => $price->currency->code,
'amount' => $price->price,
]);
}
if ($price->price < 0) {
$this->notify('The upgrade has been completed. We\'ve added the remaining amount to your account balance.', 'success');
} else {
$this->notify('The upgrade has been completed.', 'success');
}
return $this->redirect(route('services.show', $this->service), true);
}
$invoice = new Invoice([
'currency_code' => $this->service->currency_code,
'status' => Invoice::STATUS_PENDING,
'due_at' => Carbon::now()->addDays(7),
'user_id' => $this->service->user_id,
]);
$invoice->save();
$upgrade->invoice_id = $invoice->id;
$upgrade->save();
$invoice->items()->create([
'description' => 'Upgrade ' . $this->service->product->name . ' to ' . $this->upgradeProduct->name,
'price' => $price->price,
'quantity' => 1,
'reference_id' => $upgrade->id,
'reference_type' => ServiceUpgrade::class,
]);
event(new InvoiceCreated($invoice));
$this->notify('The upgrade has been added to your cart. Please complete the payment to proceed.', 'success');
return $this->redirect(route('invoices.show', $invoice));
}
public function render()
{
return view('services.upgrade')->layoutData([
'title' => 'Upgrade Service',
'sidebar' => true,
]);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Livewire\Services;
use App\Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Livewire\WithPagination;
class Widget extends Component
{
use WithPagination;
public $status = null;
public $limit = null;
public function render()
{
$query = Auth::user()->services()->orderByDesc('created_at');
if ($this->status) {
$query->where('status', $this->status);
} else {
$query->where('status', '!=', 'cancelled');
}
$services = $this->limit
? $query->take($this->limit)->get()
: $query->paginate(config('settings.pagination'));
return view('services.widget', [
'services' => $services,
])->layoutData([
'title' => 'Services',
]);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Livewire\Tickets;
use App\Attributes\DisabledIf;
use App\Livewire\Component;
use App\Models\Ticket;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Storage;
#[DisabledIf('tickets_disabled')]
class Create extends Component
{
public string $message;
public string $subject;
public string $department;
public ?int $service = null;
public string $priority;
public function create()
{
// Add rules for the department
$this->validate([
'department' => count((array) config('settings.ticket_departments')) > 0 ? 'required|in:' . implode(',', array_values((array) config('settings.ticket_departments'))) : '',
'service' => 'nullable|exists:services,id',
'subject' => 'required|string',
'message' => 'required|string',
'priority' => 'required|in:low,medium,high',
]);
if (RateLimiter::tooManyAttempts('create-ticket', 1)) {
$this->notify('Too many ticket creation attempts. Please try again in 60 seconds.', 'error');
return;
}
RateLimiter::increment('create-ticket', 30);
$ticket = Ticket::create([
'user_id' => Auth::id(),
'department' => $this->department,
'service_id' => $this->service,
'subject' => $this->subject,
'priority' => $this->priority,
]);
$ticket->messages()->create([
'user_id' => Auth::id(),
'message' => $this->message,
]);
$this->notify('Message sent successfully');
$this->message = '';
$this->dispatch('saved');
$this->redirect(route('tickets.show', $ticket), true);
}
public function render()
{
/** @var \App\Models\User */
$user = Auth::user();
return view('tickets.create', [
'departments' => (array) config('settings.ticket_departments'),
'services' => $user->services()->orderBy('id', 'desc')->get(),
])->layoutData([
'title' => 'Create Ticket',
'sidebar' => true,
]);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Livewire\Tickets;
use App\Attributes\DisabledIf;
use App\Livewire\Component;
use App\Models\Ticket;
use Illuminate\Support\Facades\Auth;
use Livewire\WithPagination;
#[DisabledIf('tickets_disabled')]
class Index extends Component
{
use WithPagination;
public function render()
{
return view('tickets.index', [
'tickets' => Ticket::where('user_id', Auth::id())->latest()->paginate(config('settings.pagination')),
])->layoutData([
'title' => 'Tickets',
'sidebar' => true,
]);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Livewire\Tickets;
use App\Attributes\DisabledIf;
use App\Livewire\Component;
use App\Models\Ticket;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Rule;
#[DisabledIf('tickets_disabled')]
class Show extends Component
{
#[Locked]
public Ticket $ticket;
#[Rule('required', 'string')]
public string $message;
public function save()
{
$this->validate();
$this->ticket->messages()->create([
'user_id' => Auth::id(),
'message' => $this->message,
]);
$this->notify('Message sent successfully');
$this->message = '';
$this->dispatch('saved');
}
public function render()
{
return view('tickets.show')->layoutData([
'sidebar' => true,
]);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Livewire\Tickets;
use App\Livewire\Component;
use App\Models\Ticket;
use Illuminate\Support\Facades\Auth;
use Livewire\WithPagination;
class Widget extends Component
{
use WithPagination;
public function render()
{
return view('tickets.widget', [
'tickets' => Ticket::where('user_id', Auth::id())->where('status', '!=', 'closed')->latest()->paginate(config('settings.pagination')),
])->layoutData([
'title' => 'Tickets',
]);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Livewire\Traits;
trait Disabled
{
public function bootDisabled()
{
// Read class-level attributes
$reflection = new \ReflectionClass($this);
$attributes = $reflection->getAttributes(\App\Attributes\DisabledIf::class);
// Check if the DisabledIf attribute is present
foreach ($attributes as $attribute) {
$instance = $attribute->newInstance();
// Check the condition based on the attribute's setting
if (config('settings.' . $instance->setting, $instance->default)) {
// If the condition is met, abort with a 404 error
abort(404, 'This feature is currently disabled.');
}
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Livewire\Traits;
use Illuminate\Support\Facades\Session;
trait HasNotifications
{
public function bootHasNotifications()
{
if (Session::has('notification')) {
$this->notify(
Session::get('notification')['message'],
Session::get('notification')['type']
);
Session::forget('notification');
}
}
/**
* Notifications
*/
public function notify($message, $type = 'success')
{
$this->dispatch('notify', ['message' => $message, 'type' => $type]);
}
}