commit
ce749dad30
64
app/Actions/License/CheckResaleLicense.php
Normal file
64
app/Actions/License/CheckResaleLicense.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\License;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class CheckResaleLicense
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
try {
|
||||
$settings = InstanceSettings::get();
|
||||
$instance_id = config('app.id');
|
||||
if (!$settings->resale_license) {
|
||||
return;
|
||||
}
|
||||
ray('Checking license key');
|
||||
$data = Http::withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
])->post('https://api.lemonsqueezy.com/v1/licenses/validate', [
|
||||
'license_key' => $settings->resale_license,
|
||||
'instance_name' => $instance_id,
|
||||
])->throw()->json();
|
||||
$product_id = data_get($data, 'meta.product_id');
|
||||
|
||||
if (isDev()) {
|
||||
$valid_product_id = 93221;
|
||||
} else {
|
||||
$valid_product_id = 93222;
|
||||
}
|
||||
if ($product_id !== $valid_product_id) {
|
||||
throw new \Exception('Invalid product id');
|
||||
}
|
||||
ray('Valid Product Id');
|
||||
|
||||
['valid' => $valid, 'license_key' => $license_key] = $data;
|
||||
|
||||
if ($valid) {
|
||||
if (data_get($license_key, 'status') === 'inactive') {
|
||||
Http::withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
])->post('https://api.lemonsqueezy.com/v1/licenses/activate', [
|
||||
'license_key' => $settings->resale_license,
|
||||
'instance_name' => $instance_id,
|
||||
])->throw()->json();
|
||||
}
|
||||
$settings->update([
|
||||
'is_resale_license_active' => true,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
throw new \Exception('Invalid license key');
|
||||
} catch (\Throwable $th) {
|
||||
ray($th);
|
||||
$settings->update([
|
||||
'resale_license' => null,
|
||||
'is_resale_license_active' => false,
|
||||
]);
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,10 +8,11 @@
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class InstallProxy
|
||||
class StartProxy
|
||||
{
|
||||
public function __invoke(Server $server): Activity
|
||||
{
|
||||
// TODO: check for other proxies
|
||||
if (is_null(data_get($server, 'proxy.type'))) {
|
||||
$server->proxy->type = ProxyTypes::TRAEFIK_V2->value;
|
||||
$server->proxy->status = ProxyStatus::EXITED->value;
|
@ -2,9 +2,11 @@
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Jobs\CheckResaleLicenseJob;
|
||||
use App\Jobs\InstanceAutoUpdateJob;
|
||||
use App\Jobs\ProxyCheckJob;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\CheckResaleLicenseKeys;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
@ -14,10 +16,13 @@ protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
if (isDev()) {
|
||||
$schedule->command('horizon:snapshot')->everyMinute();
|
||||
$schedule->job(new DockerCleanupJob)->everyOddHour();
|
||||
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
|
||||
// $schedule->job(new CheckResaleLicenseJob)->hourly();
|
||||
// $schedule->job(new DockerCleanupJob)->everyOddHour();
|
||||
// $schedule->job(new InstanceAutoUpdateJob(true))->everyMinute();
|
||||
} else {
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$schedule->job(new CheckResaleLicenseJob)->hourly();
|
||||
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
|
||||
$schedule->job(new DockerCleanupJob)->everyTenMinutes();
|
||||
$schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes();
|
||||
@ -29,4 +34,4 @@ protected function commands(): void
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,9 +17,23 @@ class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests, ValidatesRequests;
|
||||
|
||||
public function subscription()
|
||||
{
|
||||
if (!isCloud()) {
|
||||
abort(404);
|
||||
}
|
||||
return view('subscription', [
|
||||
'settings' => InstanceSettings::get()
|
||||
]);
|
||||
}
|
||||
public function license()
|
||||
{
|
||||
return view('license');
|
||||
if (!isCloud()) {
|
||||
abort(404);
|
||||
}
|
||||
return view('settings.license', [
|
||||
'settings' => InstanceSettings::get()
|
||||
]);
|
||||
}
|
||||
public function dashboard()
|
||||
{
|
||||
@ -62,13 +76,23 @@ public function emails()
|
||||
public function team()
|
||||
{
|
||||
$invitations = [];
|
||||
if (auth()->user()->isAdmin()) {
|
||||
if (auth()->user()->isAdminFromSession()) {
|
||||
$invitations = TeamInvitation::whereTeamId(auth()->user()->currentTeam()->id)->get();
|
||||
}
|
||||
return view('team.show', [
|
||||
'invitations' => $invitations,
|
||||
]);
|
||||
}
|
||||
public function members()
|
||||
{
|
||||
$invitations = [];
|
||||
if (auth()->user()->isAdminFromSession()) {
|
||||
$invitations = TeamInvitation::whereTeamId(auth()->user()->currentTeam()->id)->get();
|
||||
}
|
||||
return view('team.members', [
|
||||
'invitations' => $invitations,
|
||||
]);
|
||||
}
|
||||
public function acceptInvitation()
|
||||
{
|
||||
try {
|
||||
|
@ -13,7 +13,7 @@ class MagicController extends Controller
|
||||
public function servers()
|
||||
{
|
||||
return response()->json([
|
||||
'servers' => Server::validated()->get()
|
||||
'servers' => Server::isUsable()->get()
|
||||
]);
|
||||
}
|
||||
public function destinations()
|
||||
|
@ -21,7 +21,7 @@ class Kernel extends HttpKernel
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
// \App\Http\Middleware\LicenseValid::class,
|
||||
|
||||
];
|
||||
|
||||
/**
|
||||
@ -37,6 +37,8 @@ class Kernel extends HttpKernel
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\SubscriptionValid::class,
|
||||
|
||||
],
|
||||
|
||||
'api' => [
|
||||
|
42
app/Http/Livewire/CheckLicense.php
Normal file
42
app/Http/Livewire/CheckLicense.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire;
|
||||
|
||||
use App\Actions\License\CheckResaleLicense;
|
||||
use App\Models\InstanceSettings;
|
||||
use Livewire\Component;
|
||||
|
||||
class CheckLicense extends Component
|
||||
{
|
||||
public InstanceSettings|null $settings = null;
|
||||
public string|null $instance_id = null;
|
||||
protected $rules = [
|
||||
'settings.resale_license' => 'nullable',
|
||||
'settings.is_resale_license_active' => 'nullable',
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
'settings.resale_license' => 'License',
|
||||
'instance_id' => 'Instance Id (Do not change this)',
|
||||
'settings.is_resale_license_active' => 'Is License Active',
|
||||
];
|
||||
public function mount()
|
||||
{
|
||||
$this->instance_id = config('app.id');
|
||||
$this->settings = InstanceSettings::get();
|
||||
}
|
||||
public function submit()
|
||||
{
|
||||
$this->validate();
|
||||
$this->settings->save();
|
||||
if ($this->settings->resale_license) {
|
||||
try {
|
||||
resolve(CheckResaleLicense::class)();
|
||||
$this->emit('reloadWindow');
|
||||
} catch (\Throwable $th) {
|
||||
session()->flash('error', 'License is not valid. Please contact support.');
|
||||
ray($th->getMessage());
|
||||
return redirect()->to('/settings/license');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Project\Application;
|
||||
|
||||
use App\Models\Application;
|
||||
use Livewire\Component;
|
||||
|
||||
class Status extends Component
|
||||
{
|
||||
public Application $application;
|
||||
|
||||
public function applicationStatusChanged()
|
||||
{
|
||||
$this->application->refresh();
|
||||
$this->emit('applicationStatusChanged');
|
||||
}
|
||||
}
|
@ -63,7 +63,7 @@ public function validateServer()
|
||||
} else {
|
||||
$this->server->settings->is_usable = true;
|
||||
$this->server->settings->save();
|
||||
$this->emit('serverValidated');
|
||||
$this->emit('proxyStatusUpdated');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->server->settings->is_reachable = false;
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace App\Http\Livewire\Server;
|
||||
|
||||
use App\Actions\Proxy\CheckProxySettingsInSync;
|
||||
use App\Actions\Proxy\InstallProxy;
|
||||
use App\Enums\ProxyTypes;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\Server;
|
||||
@ -17,25 +16,27 @@ class Proxy extends Component
|
||||
public $proxy_settings = null;
|
||||
public string|null $redirect_url = null;
|
||||
|
||||
protected $listeners = ['serverValidated', 'saveConfiguration'];
|
||||
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration'];
|
||||
public function mount()
|
||||
{
|
||||
$this->redirect_url = $this->server->proxy->redirect_url;
|
||||
}
|
||||
public function serverValidated()
|
||||
public function proxyStatusUpdated()
|
||||
{
|
||||
$this->server->refresh();
|
||||
}
|
||||
public function switchProxy()
|
||||
{
|
||||
$this->server->proxy->type = null;
|
||||
$this->server->proxy = null;
|
||||
$this->server->save();
|
||||
$this->emit('proxyStatusUpdated');
|
||||
}
|
||||
public function setProxy(string $proxy_type)
|
||||
{
|
||||
$this->server->proxy->type = $proxy_type;
|
||||
$this->server->proxy->status = 'exited';
|
||||
$this->server->save();
|
||||
$this->emit('proxyStatusUpdated');
|
||||
}
|
||||
public function stopProxy()
|
||||
{
|
||||
@ -44,7 +45,7 @@ public function stopProxy()
|
||||
], $this->server);
|
||||
$this->server->proxy->status = 'exited';
|
||||
$this->server->save();
|
||||
$this->server->refresh();
|
||||
$this->emit('proxyStatusUpdated');
|
||||
}
|
||||
public function saveConfiguration()
|
||||
{
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Http\Livewire\Server\Proxy;
|
||||
|
||||
use App\Actions\Proxy\InstallProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
use Str;
|
||||
@ -11,7 +11,7 @@ class Deploy extends Component
|
||||
{
|
||||
public Server $server;
|
||||
public $proxy_settings = null;
|
||||
protected $listeners = ['proxyStatusUpdated', 'serverValidated' => 'proxyStatusUpdated'];
|
||||
protected $listeners = ['proxyStatusUpdated'];
|
||||
public function proxyStatusUpdated()
|
||||
{
|
||||
$this->server->refresh();
|
||||
@ -24,7 +24,7 @@ public function deploy()
|
||||
) {
|
||||
$this->saveConfiguration($this->server);
|
||||
}
|
||||
$activity = resolve(InstallProxy::class)($this->server);
|
||||
$activity = resolve(StartProxy::class)($this->server);
|
||||
$this->emit('newMonitorActivity', $activity->id);
|
||||
}
|
||||
public function stop()
|
||||
|
@ -9,7 +9,7 @@
|
||||
class Status extends Component
|
||||
{
|
||||
public Server $server;
|
||||
protected $listeners = ['proxyStatusUpdated', 'serverValidated' => 'proxyStatusUpdated'];
|
||||
protected $listeners = ['proxyStatusUpdated'];
|
||||
public function proxyStatusUpdated()
|
||||
{
|
||||
$this->server->refresh();
|
||||
@ -17,7 +17,7 @@ public function proxyStatusUpdated()
|
||||
public function proxyStatus()
|
||||
{
|
||||
try {
|
||||
dispatch(new ProxyContainerStatusJob(
|
||||
dispatch_sync(new ProxyContainerStatusJob(
|
||||
server: $this->server
|
||||
));
|
||||
$this->emit('proxyStatusUpdated');
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Http\Livewire\Settings;
|
||||
|
||||
use App\Jobs\ProxyCheckJob;
|
||||
use App\Jobs\ProxyStartJob;
|
||||
use App\Models\InstanceSettings as ModelsInstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
@ -21,11 +21,13 @@ class Configuration extends Component
|
||||
|
||||
protected $rules = [
|
||||
'settings.fqdn' => 'nullable',
|
||||
'settings.resale_license' => 'nullable',
|
||||
'settings.public_port_min' => 'required',
|
||||
'settings.public_port_max' => 'required',
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
'settings.fqdn' => 'FQDN',
|
||||
'settings.resale_license' => 'Resale License',
|
||||
'settings.public_port_min' => 'Public port min',
|
||||
'settings.public_port_max' => 'Public port max',
|
||||
];
|
||||
@ -105,6 +107,7 @@ private function setup_instance_fqdn()
|
||||
];
|
||||
}
|
||||
$this->save_configuration_to_disk($traefik_dynamic_conf, $file);
|
||||
dispatch(new ProxyStartJob($this->server));
|
||||
}
|
||||
}
|
||||
private function save_configuration_to_disk(array $traefik_dynamic_conf, string $file)
|
||||
@ -134,12 +137,8 @@ public function submit()
|
||||
}
|
||||
$this->validate();
|
||||
$this->settings->save();
|
||||
|
||||
$this->server = Server::findOrFail(0);
|
||||
$this->setup_instance_fqdn();
|
||||
if ($this->settings->fqdn) {
|
||||
dispatch(new ProxyCheckJob());
|
||||
}
|
||||
$this->emit('success', 'Instance settings updated successfully!');
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class LicenseValid
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (!config('coolify.self_hosted')) {
|
||||
$value = Cache::get('license_key');
|
||||
if (!$value) {
|
||||
ray($request->path());
|
||||
if ($request->path() !== 'license' && $request->path() !== 'livewire/message/license') {
|
||||
return redirect('license');
|
||||
}
|
||||
}
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
41
app/Http/Middleware/SubscriptionValid.php
Normal file
41
app/Http/Middleware/SubscriptionValid.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SubscriptionValid
|
||||
{
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (auth()->user()) {
|
||||
if (isCloud() && !isSubscribed()) {
|
||||
ray('SubscriptionValid Middleware');
|
||||
|
||||
$allowed_paths = [
|
||||
'subscription',
|
||||
'login',
|
||||
'register',
|
||||
'logout',
|
||||
'livewire/message/check-license',
|
||||
'livewire/message/switch-team',
|
||||
];
|
||||
if (!in_array($request->path(), $allowed_paths)) {
|
||||
return redirect('subscription');
|
||||
} else {
|
||||
return $next($request);
|
||||
}
|
||||
} else {
|
||||
if ($request->path() === 'subscription' && !auth()->user()->isInstanceAdmin()) {
|
||||
return redirect('/');
|
||||
} else {
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
@ -107,7 +107,7 @@ public function __construct(int $application_deployment_queue_id)
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
ray()->measure();
|
||||
// ray()->measure();
|
||||
$this->application_deployment_queue->update([
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
@ -117,6 +117,7 @@ public function handle(): void
|
||||
} else {
|
||||
$this->deploy();
|
||||
}
|
||||
if ($this->application->fqdn) dispatch(new ProxyStartJob($this->server));
|
||||
$this->next(ApplicationDeploymentStatus::FINISHED->value);
|
||||
} catch (\Exception $e) {
|
||||
ray($e);
|
||||
@ -131,7 +132,7 @@ public function handle(): void
|
||||
"hidden" => true,
|
||||
]
|
||||
);
|
||||
ray()->measure();
|
||||
// ray()->measure();
|
||||
}
|
||||
}
|
||||
public function failed(Throwable $exception): void
|
||||
@ -647,4 +648,4 @@ private function clone_repository()
|
||||
);
|
||||
$this->commit = $this->saved_outputs->get('git_commit_sha');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
32
app/Jobs/CheckResaleLicenseJob.php
Normal file
32
app/Jobs/CheckResaleLicenseJob.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\License\CheckResaleLicense;
|
||||
use App\Models\InstanceSettings;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class CheckResaleLicenseJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
resolve(CheckResaleLicense::class)();
|
||||
} catch (\Throwable $th) {
|
||||
ray($th);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Proxy\InstallProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
@ -15,30 +15,23 @@ class ProxyCheckJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$container_name = 'coolify-proxy';
|
||||
$servers = Server::whereRelation('settings', 'is_usable', true)->where('proxy->type', ProxyTypes::TRAEFIK_V2)->get();
|
||||
|
||||
$servers = Server::isUsable()->whereNotNull('proxy')->get();
|
||||
foreach ($servers as $server) {
|
||||
$status = get_container_status(server: $server, container_id: $container_name);
|
||||
if ($status === 'running') {
|
||||
continue;
|
||||
}
|
||||
resolve(InstallProxy::class)($server);
|
||||
resolve(StartProxy::class)($server);
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
ray($th->getMessage());
|
||||
//throw $th;
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,10 @@ public function handle(): void
|
||||
$this->server->save();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
if ($e->getCode() === 1) {
|
||||
$this->server->proxy->status = 'exited';
|
||||
$this->server->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
35
app/Jobs/ProxyStartJob.php
Executable file
35
app/Jobs/ProxyStartJob.php
Executable file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProxyStartJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(protected Server $server)
|
||||
{
|
||||
}
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$container_name = 'coolify-proxy';
|
||||
ray('Starting proxy for server: ' . $this->server->name);
|
||||
$status = get_container_status(server: $this->server, container_id: $container_name);
|
||||
if ($status === 'running') {
|
||||
return;
|
||||
}
|
||||
resolve(StartProxy::class)($this->server);
|
||||
} catch (\Throwable $th) {
|
||||
ray($th->getMessage());
|
||||
//throw $th;
|
||||
}
|
||||
}
|
||||
}
|
@ -12,11 +12,13 @@
|
||||
class InstanceSettings extends Model implements SendsEmail
|
||||
{
|
||||
use Notifiable, SchemalessAttributesTrait;
|
||||
protected $guarded = [];
|
||||
protected $schemalessAttributes = [
|
||||
'smtp',
|
||||
];
|
||||
protected $casts = [
|
||||
'smtp' => SchemalessAttributes::class,
|
||||
'resale_license' => 'encrypted',
|
||||
];
|
||||
public function scopeWithSmtp(): Builder
|
||||
{
|
||||
|
@ -94,10 +94,14 @@ static public function ownedByCurrentTeam(array $select = ['*'])
|
||||
return Server::whereTeamId(session('currentTeam')->id)->with('settings')->select($selectArray->all())->orderBy('name');
|
||||
}
|
||||
|
||||
static public function validated()
|
||||
static public function isReachable()
|
||||
{
|
||||
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true);
|
||||
}
|
||||
static public function isUsable()
|
||||
{
|
||||
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true);
|
||||
}
|
||||
|
||||
static public function destinationsByServer(string $server_id)
|
||||
{
|
||||
@ -106,4 +110,4 @@ static public function destinationsByServer(string $server_id)
|
||||
$swarmDocker = collect($server->swarmDockers->all());
|
||||
return $standaloneDocker->concat($swarmDocker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
app/Models/Subscription.php
Normal file
15
app/Models/Subscription.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Subscription extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
public function team()
|
||||
{
|
||||
return $this->belongsTo(Team::class);
|
||||
}
|
||||
}
|
@ -66,7 +66,10 @@ public function routeNotificationForEmail(string $attribute = 'recipients')
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function subscription()
|
||||
{
|
||||
return $this->hasOne(Subscription::class);
|
||||
}
|
||||
public function projects()
|
||||
{
|
||||
return $this->hasMany(Project::class);
|
||||
|
@ -61,8 +61,11 @@ public function routeNotificationForEmail()
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function isAdmin()
|
||||
{
|
||||
return $this->pivot->role === 'admin' || $this->pivot->role === 'owner';
|
||||
}
|
||||
public function isAdminFromSession()
|
||||
{
|
||||
if (auth()->user()->id === 0) {
|
||||
return true;
|
||||
@ -89,6 +92,10 @@ public function isInstanceAdmin()
|
||||
});
|
||||
return $found_root_team->count() > 0;
|
||||
}
|
||||
public function personalTeam()
|
||||
{
|
||||
return $this->teams()->where('personal_team', true)->first();
|
||||
}
|
||||
public function teams()
|
||||
{
|
||||
return $this->belongsToMany(Team::class)->withPivot('role');
|
||||
|
14
app/Models/Webhook.php
Normal file
14
app/Models/Webhook.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Webhook extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
'payload' => 'encrypted',
|
||||
];
|
||||
}
|
@ -27,4 +27,4 @@ public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.forms.button');
|
||||
}
|
||||
}
|
||||
}
|
@ -16,13 +16,12 @@ public function __construct(
|
||||
public string|null $type = 'text',
|
||||
public string|null $value = null,
|
||||
public string|null $label = null,
|
||||
public string|null $placeholder = null,
|
||||
public bool $required = false,
|
||||
public bool $disabled = false,
|
||||
public bool $readonly = false,
|
||||
public string|null $helper = null,
|
||||
public bool $allowToPeak = true,
|
||||
public string $defaultClass = "input input-sm bg-coolgray-200 rounded text-white w-full disabled:bg-coolgray-200/50 disabled:border-none"
|
||||
public string $defaultClass = "input input-sm bg-coolgray-200 rounded text-white w-full disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500"
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -114,7 +114,7 @@ function instant_remote_process(array $command, Server $server, $throwError = tr
|
||||
if (!$throwError) {
|
||||
return null;
|
||||
}
|
||||
throw new \RuntimeException($process->errorOutput());
|
||||
throw new \RuntimeException($process->errorOutput(), $exitCode);
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@ -127,3 +128,7 @@ function isDev()
|
||||
{
|
||||
return config('app.env') === 'local';
|
||||
}
|
||||
function isCloud()
|
||||
{
|
||||
return !config('coolify.self_hosted');
|
||||
}
|
||||
|
45
bootstrap/helpers/subscriptions.php
Normal file
45
bootstrap/helpers/subscriptions.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
function getSubscriptionLink()
|
||||
{
|
||||
$user_id = auth()->user()->id;
|
||||
$team_id = auth()->user()->currentTeam()->id ?? null;
|
||||
$email = auth()->user()->email ?? null;
|
||||
$name = auth()->user()->name ?? null;
|
||||
$url = "https://store.coollabs.io/checkout/buy/d0b28c6a-9b57-40bf-8b84-89fbafde6526?";
|
||||
if ($user_id) {
|
||||
$url .= "&checkout[custom][user_id]={$user_id}";
|
||||
}
|
||||
if (isset($team_id)) {
|
||||
$url .= "&checkout[custom][team_id]={$team_id}";
|
||||
}
|
||||
if ($email) {
|
||||
$url .= "&checkout[email]={$email}";
|
||||
}
|
||||
if ($name) {
|
||||
$url .= "&checkout[name]={$name}";
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
function getPaymentLink()
|
||||
{
|
||||
return auth()->user()->currentTeam()->subscription->lemon_update_payment_menthod_url;
|
||||
}
|
||||
function getRenewDate()
|
||||
{
|
||||
return Carbon::parse(auth()->user()->currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s');
|
||||
}
|
||||
function getEndDate()
|
||||
{
|
||||
return Carbon::parse(auth()->user()->currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s');
|
||||
}
|
||||
function isSubscribed()
|
||||
{
|
||||
return
|
||||
auth()->user()?->currentTeam()?->subscription?->lemon_status === 'active' ||
|
||||
(auth()->user()?->currentTeam()?->subscription?->lemon_ends_at &&
|
||||
Carbon::parse(auth()->user()->currentTeam()->subscription->lemon_ends_at) > Carbon::now()
|
||||
) || auth()->user()->isInstanceAdmin();
|
||||
}
|
568
composer.lock
generated
568
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,7 @@
|
||||
|
||||
return [
|
||||
|
||||
'id' => env('APP_ID'),
|
||||
'port' => env('APP_PORT', 8000),
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
return [
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET'),
|
||||
'mux_enabled' => env('MUX_ENABLED', true),
|
||||
'dev_webhook' => env('SERVEO_URL'),
|
||||
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
||||
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('subscriptions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('lemon_subscription_id');
|
||||
$table->string('lemon_order_id');
|
||||
$table->string('lemon_product_id');
|
||||
$table->string('lemon_variant_id');
|
||||
$table->string('lemon_variant_name');
|
||||
$table->string('lemon_customer_id');
|
||||
$table->string('lemon_status');
|
||||
$table->string('lemon_trial_ends_at')->nullable();
|
||||
$table->string('lemon_renews_at');
|
||||
$table->string('lemon_ends_at')->nullable();
|
||||
$table->string('lemon_update_payment_menthod_url');
|
||||
$table->foreignId('team_id');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('subscriptions');
|
||||
}
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('webhooks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->enum('status', ['pending', 'success', 'failed'])->default('pending');
|
||||
$table->enum('type', ['github', 'gitlab', 'bitbucket', 'lemonsqueezy']);
|
||||
$table->longText('payload');
|
||||
$table->longText('failure_reason')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('webhooks');
|
||||
}
|
||||
};
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_resale_license_active')->default(false);
|
||||
$table->longText('resale_license')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('is_resale_license_active');
|
||||
$table->dropColumn('resale_license');
|
||||
});
|
||||
}
|
||||
};
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Actions\Proxy\InstallProxy;
|
||||
use App\Data\ServerMetadata;
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
@ -25,10 +24,10 @@ public function run(): void
|
||||
'ip' => "coolify-testing-host",
|
||||
'team_id' => $root_team->id,
|
||||
'private_key_id' => $private_key_1->id,
|
||||
'proxy' => ServerMetadata::from([
|
||||
'type' => ProxyTypes::TRAEFIK_V2->value,
|
||||
'status' => ProxyStatus::EXITED->value
|
||||
]),
|
||||
// 'proxy' => ServerMetadata::from([
|
||||
// 'type' => ProxyTypes::TRAEFIK_V2->value,
|
||||
// 'status' => ProxyStatus::EXITED->value
|
||||
// ]),
|
||||
]);
|
||||
Server::create([
|
||||
'name' => "testing-local-docker-container-2",
|
||||
@ -38,4 +37,4 @@ public function run(): void
|
||||
'private_key_id' => $private_key_1->id
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,12 +16,14 @@ public function run(): void
|
||||
$server_2 = Server::find(0)->load(['settings']);
|
||||
$server_2->settings->wildcard_domain = 'http://127.0.0.1.sslip.io';
|
||||
$server_2->settings->is_build_server = true;
|
||||
$server_2->settings->is_usable = true;
|
||||
$server_2->settings->is_reachable = true;
|
||||
$server_2->settings->save();
|
||||
|
||||
$server_3 = Server::find(1)->load(['settings']);
|
||||
$server_3->settings->is_part_of_swarm = false;
|
||||
$server_2->settings->is_usable = false;
|
||||
$server_3->settings->is_reachable = false;
|
||||
$server_3->settings->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
17
database/seeders/SubscriptionSeeder.php
Normal file
17
database/seeders/SubscriptionSeeder.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class SubscriptionSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
@ -16,7 +16,6 @@ public function run(): void
|
||||
$root_user_personal_team->save();
|
||||
|
||||
$normal_user_in_root_team->teams()->attach($root_user_personal_team);
|
||||
|
||||
$normal_user_not_in_root_team = User::find(2);
|
||||
$normal_user_in_root_team_personal_team = Team::find(1);
|
||||
$normal_user_not_in_root_team->teams()->attach($normal_user_in_root_team_personal_team, ['role' => 'admin']);
|
||||
|
17
database/seeders/WebhookSeeder.php
Normal file
17
database/seeders/WebhookSeeder.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class WebhookSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
2
public/vendor/horizon/app.js
vendored
2
public/vendor/horizon/app.js
vendored
File diff suppressed because one or more lines are too long
2
public/vendor/horizon/mix-manifest.json
vendored
2
public/vendor/horizon/mix-manifest.json
vendored
@ -1,5 +1,5 @@
|
||||
{
|
||||
"/app.js": "/app.js?id=c6187bff8d842d49dbb4d3de4b583600",
|
||||
"/app.js": "/app.js?id=7e1968acfd75b8dc843675097962e3ce",
|
||||
"/app-dark.css": "/app-dark.css?id=15c72df05e2b1147fa3e4b0670cfb435",
|
||||
"/app.css": "/app.css?id=4d6a1a7fe095eedc2cb2a4ce822ea8a5",
|
||||
"/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f",
|
||||
|
@ -8,11 +8,11 @@
|
||||
@endisset
|
||||
@if ($isModal) onclick="{{ $modalId }}.showModal()" @endif>
|
||||
|
||||
{{ $slot }}
|
||||
@if ($attributes->get('type') === 'submit')
|
||||
<span wire:target="submit" wire:loading.delay class="loading loading-xs text-warning loading-spinner"></span>
|
||||
@else
|
||||
<span wire:target="{{ explode('(', $attributes->whereStartsWith('wire:click')->first())[0] }}" wire:loading.delay
|
||||
class="loading loading-xs loading-spinner"></span>
|
||||
@endif
|
||||
{{ $slot }}
|
||||
</button>
|
||||
|
@ -12,7 +12,7 @@ class="flex items-center gap-1 mb-2 text-sm font-medium text-neutral-400">{{ $la
|
||||
@endif
|
||||
<select {{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
|
||||
wire:dirty.class="text-black bg-warning" wire:loading.attr="disabled" name={{ $id }}
|
||||
wire:model.defer={{ $id }}>
|
||||
@if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} @else wire:model.defer={{ $id }} @endif>
|
||||
{{ $slot }}
|
||||
</select>
|
||||
</div>
|
||||
|
65
resources/views/components/layout-subscription.blade.php
Normal file
65
resources/views/components/layout-subscription.blade.php
Normal file
@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin>
|
||||
<link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet">
|
||||
@env('local')
|
||||
<title>Coolify - localhost</title>
|
||||
@endenv
|
||||
@env('production')
|
||||
<title>{{ $title ?? 'Coolify' }}</title>
|
||||
@endenv
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
@vite(['resources/js/app.js', 'resources/css/app.css'])
|
||||
<style>
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
@livewireStyles
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@livewireScripts
|
||||
<x-toaster-hub />
|
||||
@if (auth()->user()->isInstanceAdmin())
|
||||
<div class="fixed top-3 left-4" id="vue">
|
||||
<magic-bar></magic-bar>
|
||||
</div>
|
||||
<x-navbar />
|
||||
@else
|
||||
<x-navbar-subscription />
|
||||
@endif
|
||||
|
||||
<main class="main">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
<x-version class="fixed left-2 bottom-1" />
|
||||
<script>
|
||||
function changePasswordFieldType(event) {
|
||||
let element = event.target
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (element.className === "relative") {
|
||||
break;
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
element = element.children[1];
|
||||
if (element.nodeName === 'INPUT') {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
} else {
|
||||
element.type = 'password';
|
||||
}
|
||||
}
|
||||
}
|
||||
Livewire.on('reloadWindow', () => {
|
||||
window.location.reload();
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
20
resources/views/components/navbar-subscription.blade.php
Normal file
20
resources/views/components/navbar-subscription.blade.php
Normal file
@ -0,0 +1,20 @@
|
||||
@auth
|
||||
<nav class="fixed h-full overflow-hidden overflow-y-auto scrollbar">
|
||||
<ul class="flex flex-col h-full gap-4 menu flex-nowrap">
|
||||
<li class="pb-6" title="Logout">
|
||||
<form action="/logout" method="POST" class=" hover:bg-transparent">
|
||||
@csrf
|
||||
<button class="flex items-center gap-2 rounded-none hover:text-white hover:bg-transparent"> <svg
|
||||
xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M13 12v.01" />
|
||||
<path d="M3 21h18" />
|
||||
<path d="M5 21v-16a2 2 0 0 1 2 -2h7.5m2.5 10.5v7.5" />
|
||||
<path d="M14 7h7m-3 -3l3 3l-3 3" />
|
||||
</svg> Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@endauth
|
@ -1,110 +1,113 @@
|
||||
@auth
|
||||
<nav class="fixed h-full overflow-hidden overflow-y-auto pt-14 scrollbar">
|
||||
<ul class="flex flex-col h-full gap-4 menu flex-nowrap">
|
||||
<li title="Dashboard">
|
||||
<a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="{{ request()->is('/') ? 'text-warning icon' : 'icon' }}"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li title="Projects">
|
||||
<a class="hover:bg-transparent" @if (!request()->is('projects')) href="/projects" @endif>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="{{ request()->is('project/*') || request()->is('projects') ? 'text-warning icon' : 'icon' }}"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 4l-8 4l8 4l8 -4l-8 -4" />
|
||||
<path d="M4 12l8 4l8 -4" />
|
||||
<path d="M4 16l8 4l8 -4" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li title="Servers">
|
||||
<a class="hover:bg-transparent" @if (!request()->is('servers')) href="/servers" @endif>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="{{ request()->is('server/*') || request()->is('servers') ? 'text-warning icon' : 'icon' }}"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3 4m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z" />
|
||||
<path d="M15 20h-9a3 3 0 0 1 -3 -3v-2a3 3 0 0 1 3 -3h12" />
|
||||
<path d="M7 8v.01" />
|
||||
<path d="M7 16v.01" />
|
||||
<path d="M20 15l-2 3h3l-2 3" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
@if (auth()->user()->isInstanceAdmin())
|
||||
<li title="Command Center">
|
||||
<a class="hover:bg-transparent" @if (!request()->is('command-center')) href="/command-center" @endif>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="{{ request()->is('command-center') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M5 7l5 5l-5 5" />
|
||||
<path d="M12 19l7 0" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li title="Profile">
|
||||
<a class="hover:bg-transparent" @if (!request()->is('profile')) href="/profile" @endif>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M12 10m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" />
|
||||
<path d="M6.168 18.849a4 4 0 0 1 3.832 -2.849h4a4 4 0 0 1 3.834 2.855" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li title="Teams">
|
||||
<a class="hover:bg-transparent" href="{{ route('team.show') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1" />
|
||||
<path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M17 10h2a2 2 0 0 1 2 2v1" />
|
||||
<path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M3 13v-1a2 2 0 0 1 2 -2h2" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<livewire:upgrade />
|
||||
<li title="Settings" class="mt-auto">
|
||||
<a class="hover:bg-transparent" @if (!request()->is('settings')) href="/settings" @endif>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
|
||||
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li class="pb-6" title="Logout">
|
||||
<form action="/logout" method="POST" class=" hover:bg-transparent">
|
||||
@csrf
|
||||
<button class="rounded-none hover:text-white hover:bg-transparent"> <svg
|
||||
xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<nav class="fixed h-full overflow-hidden overflow-y-auto pt-14 scrollbar">
|
||||
<ul class="flex flex-col h-full gap-4 menu flex-nowrap">
|
||||
<li title="Dashboard">
|
||||
<a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="{{ request()->is('/') ? 'text-warning icon' : 'icon' }}"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li title="Projects">
|
||||
<a class="hover:bg-transparent" @if (!request()->is('projects')) href="/projects" @endif>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="{{ request()->is('project/*') || request()->is('projects') ? 'text-warning icon' : 'icon' }}"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M13 12v.01" />
|
||||
<path d="M3 21h18" />
|
||||
<path d="M5 21v-16a2 2 0 0 1 2 -2h7.5m2.5 10.5v7.5" />
|
||||
<path d="M14 7h7m-3 -3l3 3l-3 3" />
|
||||
</svg></button>
|
||||
</form>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</nav>
|
||||
@endauth
|
||||
<path d="M12 4l-8 4l8 4l8 -4l-8 -4" />
|
||||
<path d="M4 12l8 4l8 -4" />
|
||||
<path d="M4 16l8 4l8 -4" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li title="Servers">
|
||||
<a class="hover:bg-transparent" @if (!request()->is('servers')) href="/servers" @endif>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="{{ request()->is('server/*') || request()->is('servers') ? 'text-warning icon' : 'icon' }}"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3 4m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z" />
|
||||
<path d="M15 20h-9a3 3 0 0 1 -3 -3v-2a3 3 0 0 1 3 -3h12" />
|
||||
<path d="M7 8v.01" />
|
||||
<path d="M7 16v.01" />
|
||||
<path d="M20 15l-2 3h3l-2 3" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
@if (auth()->user()->isInstanceAdmin())
|
||||
<li title="Command Center">
|
||||
<a class="hover:bg-transparent" @if (!request()->is('command-center')) href="/command-center" @endif>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="{{ request()->is('command-center') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M5 7l5 5l-5 5" />
|
||||
<path d="M12 19l7 0" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li title="Profile">
|
||||
<a class="hover:bg-transparent" @if (!request()->is('profile')) href="/profile" @endif>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M12 10m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" />
|
||||
<path d="M6.168 18.849a4 4 0 0 1 3.832 -2.849h4a4 4 0 0 1 3.834 2.855" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li title="Teams">
|
||||
<a class="hover:bg-transparent" href="{{ route('team.show') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1" />
|
||||
<path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M17 10h2a2 2 0 0 1 2 2v1" />
|
||||
<path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M3 13v-1a2 2 0 0 1 2 -2h2" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<livewire:upgrade />
|
||||
<li title="Settings" class="mt-auto">
|
||||
<a class="hover:bg-transparent" @if (!request()->is('settings')) href="/settings" @endif>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
|
||||
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
<div class="flex-1"></div>
|
||||
<li class="pb-6" title="Logout">
|
||||
<form action="/logout" method="POST" class=" hover:bg-transparent">
|
||||
@csrf
|
||||
<button type="submit" class="rounded-none hover:text-white hover:bg-transparent"> <svg
|
||||
xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M13 12v.01" />
|
||||
<path d="M3 21h18" />
|
||||
<path d="M5 21v-16a2 2 0 0 1 2 -2h7.5m2.5 10.5v7.5" />
|
||||
<path d="M14 7h7m-3 -3l3 3l-3 3" />
|
||||
</svg></button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@endauth
|
||||
|
@ -9,6 +9,12 @@
|
||||
<a class="{{ request()->routeIs('settings.emails') ? 'text-white' : '' }}" href="{{ route('settings.emails') }}">
|
||||
<button>SMTP</button>
|
||||
</a>
|
||||
@if (isCloud())
|
||||
<a class="{{ request()->routeIs('settings.license') ? 'text-white' : '' }}"
|
||||
href="{{ route('settings.license') }}">
|
||||
<button>Resale License</button>
|
||||
</a>
|
||||
@endif
|
||||
<div class="flex-1"></div>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -24,11 +24,16 @@
|
||||
<a class="{{ request()->routeIs('team.show') ? 'text-white' : '' }}" href="{{ route('team.show') }}">
|
||||
<button>General</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('team.members') ? 'text-white' : '' }}" href="{{ route('team.members') }}">
|
||||
<button>Members</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('team.notifications') ? 'text-white' : '' }}"
|
||||
href="{{ route('team.notifications') }}">
|
||||
<button>Notifications</button>
|
||||
</a>
|
||||
<div class="flex-1"></div>
|
||||
<livewire:switch-team />
|
||||
<div class="-mt-9">
|
||||
<livewire:switch-team />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -1,4 +0,0 @@
|
||||
<x-layout-simple>
|
||||
<h1>Lincese Key</h1>
|
||||
<livewire:license />
|
||||
</x-layout-simple>
|
24
resources/views/livewire/check-license.blade.php
Normal file
24
resources/views/livewire/check-license.blade.php
Normal file
@ -0,0 +1,24 @@
|
||||
<form wire:submit.prevent='submit' class="flex flex-col gap-2">
|
||||
<div>
|
||||
@if ($settings->is_resale_license_active)
|
||||
<div class="text-success">License is active</div>
|
||||
@else
|
||||
<div class="text-error">License is not active</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input type="password" id="settings.resale_license" placeholder="eg: BE558E91-0CC5-4AA2-B1C0-B6403C2988DD"
|
||||
label="License Key" />
|
||||
<x-forms.input type="password" id="instance_id" label="Instance Id (do not change this)" disabled />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.button type="submit">
|
||||
Check License
|
||||
</x-forms.button>
|
||||
</div>
|
||||
@if (session()->has('error'))
|
||||
<div class="text-error">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
@ -1,32 +1,36 @@
|
||||
<div>
|
||||
<div class="flex items-end gap-2">
|
||||
<h2>Destinations</h2>
|
||||
<a href="{{ route('destination.new', ['server_id' => $server->id]) }}">
|
||||
<x-forms.button>Add a new destination</x-forms.button>
|
||||
</a>
|
||||
<x-forms.button wire:click='scan'>Scan destinations on the server</x-forms.button>
|
||||
</div>
|
||||
<div class="pt-2 pb-6 ">Destinations are used to segregate resources by network.</div>
|
||||
<div class="flex gap-2 ">
|
||||
Available for using:
|
||||
@forelse ($server->standaloneDockers as $docker)
|
||||
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
|
||||
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
|
||||
@if ($server->settings->is_usable)
|
||||
<div class="flex items-end gap-2">
|
||||
<h2>Destinations</h2>
|
||||
<a href="{{ route('destination.new', ['server_id' => $server->id]) }}">
|
||||
<x-forms.button>Add a new destination</x-forms.button>
|
||||
</a>
|
||||
@empty
|
||||
<div class="">N/A</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="grid gap-2 pt-2">
|
||||
@if (count($networks) > 0)
|
||||
<h4>Found Destinations</h4>
|
||||
@endif
|
||||
@foreach ($networks as $network)
|
||||
<a
|
||||
href="{{ route('destination.new', ['server_id' => $server->id, 'network_name' => data_get($network, 'Name')]) }}">
|
||||
<x-forms.button>Add<span class="text-warning">{{ data_get($network, 'Name') }}</span>
|
||||
</x-forms.button>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
<x-forms.button wire:click='scan'>Scan destinations on the server</x-forms.button>
|
||||
</div>
|
||||
<div class="pt-2 pb-6 ">Destinations are used to segregate resources by network.</div>
|
||||
<div class="flex gap-2 ">
|
||||
Available for using:
|
||||
@forelse ($server->standaloneDockers as $docker)
|
||||
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
|
||||
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
|
||||
</a>
|
||||
@empty
|
||||
<div class="">N/A</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="grid gap-2 pt-2">
|
||||
@if (count($networks) > 0)
|
||||
<h4>Found Destinations</h4>
|
||||
@endif
|
||||
@foreach ($networks as $network)
|
||||
<a
|
||||
href="{{ route('destination.new', ['server_id' => $server->id, 'network_name' => data_get($network, 'Name')]) }}">
|
||||
<x-forms.button>Add<span class="text-warning">{{ data_get($network, 'Name') }}</span>
|
||||
</x-forms.button>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div>Server is not validated. Validate first.</div>
|
||||
@endif
|
||||
</div>
|
||||
|
@ -1,9 +0,0 @@
|
||||
<div wire:poll.10000ms='applicationStatusChanged'>
|
||||
@if ($application->status === 'running')
|
||||
<x-status.running />
|
||||
@elseif($application->status === 'restarting')
|
||||
<x-status.restarting />
|
||||
@else
|
||||
<x-status.stopped />
|
||||
@endif
|
||||
</div>
|
@ -1,7 +1,7 @@
|
||||
<div x-data="{ stopProxy: false }">
|
||||
<x-naked-modal show="stopProxy" action="stopProxy" title="Stop Proxy"
|
||||
message='This proxy will be stopped. It is not reversible. <br>All resources will be unavailable. <br>Please think again.' />
|
||||
@if ($server->settings->is_reachable)
|
||||
@if ($server->settings->is_usable)
|
||||
@if ($server->proxy->type)
|
||||
<div x-init="$wire.checkProxySettingsInSync">
|
||||
@if ($selectedProxy->value === 'TRAEFIK_V2')
|
||||
@ -23,11 +23,11 @@
|
||||
configs.
|
||||
</div>
|
||||
@endif
|
||||
<x-forms.input placeholder="https://coolify.io" id="redirect_url" label="Default Redirect 404"
|
||||
helper="All urls that has no service available will be redirected to this domain.<span class='text-helper'>You can set to your main marketing page or your social media link.</span>" />
|
||||
<div class="container w-full mx-auto">
|
||||
<div class="container w-full pb-4 mx-auto">
|
||||
<livewire:activity-monitor :header="true" />
|
||||
</div>
|
||||
<x-forms.input placeholder="https://coolify.io" id="redirect_url" label="Default Redirect 404"
|
||||
helper="All urls that has no service available will be redirected to this domain.<span class='text-helper'>You can set to your main marketing page or your social media link.</span>" />
|
||||
<div wire:loading wire:target="checkProxySettingsInSync" class="pt-4">
|
||||
<x-loading text="Loading proxy configuration..." />
|
||||
</div>
|
||||
@ -64,6 +64,6 @@
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="">Server is not validated. Validate first.</div>
|
||||
<div>Server is not validated. Validate first.</div>
|
||||
@endif
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div>
|
||||
@if ($server->settings->is_reachable)
|
||||
@if ($server->proxy->status === 'running')
|
||||
@if (data_get($server, 'proxy.type'))
|
||||
@if (data_get($server, 'proxy.status') === 'running')
|
||||
<div class="flex gap-4">
|
||||
<div class="group">
|
||||
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Links
|
||||
|
@ -1,13 +1,9 @@
|
||||
<div>
|
||||
@if ($server->settings->is_reachable)
|
||||
<div wire:poll.10000ms="proxyStatus" x-init="$wire.proxyStatus">
|
||||
@if ($server->proxy->status === 'running')
|
||||
<x-status.running />
|
||||
@elseif ($server->proxy->status === 'restarting')
|
||||
<x-status.restarting />
|
||||
@else
|
||||
<x-status.stopped />
|
||||
@endif
|
||||
</div>
|
||||
<div wire:poll.10000ms="proxyStatus" x-init="$wire.proxyStatus">
|
||||
@if ($server->proxy->status === 'running')
|
||||
<x-status.running />
|
||||
@elseif ($server->proxy->status === 'restarting')
|
||||
<x-status.restarting />
|
||||
@else
|
||||
<x-status.stopped />
|
||||
@endif
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input id="settings.fqdn" label="Coolify's Domain" />
|
||||
</div>
|
||||
|
||||
{{-- <div class="flex gap-2 ">
|
||||
<x-forms.input type="number" id="settings.public_port_min" label="Public Port Min" />
|
||||
<x-forms.input type="number" id="settings.public_port_max" label="Public Port Max" />
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="w-64 -mt-9">
|
||||
<div class="w-64">
|
||||
<x-forms.select wire:model="selectedTeamId">
|
||||
<option value="default" disabled selected>Switch team</option>
|
||||
@foreach (auth()->user()->teams as $team)
|
||||
|
@ -8,6 +8,8 @@
|
||||
<div>This is the default team. You can't delete it.</div>
|
||||
@elseif(auth()->user()->teams()->get()->count() === 1)
|
||||
<div>You can't delete your last team.</div>
|
||||
@elseif(auth()->user()->currentTeam()->subscription?->lemon_status !== 'cancelled')
|
||||
<div>Please cancel your subscription before delete this team (Manage My Subscription button).</div>
|
||||
@else
|
||||
@if (session('currentTeam')->isEmpty())
|
||||
<div class="pb-4">This will delete your team. Beware! There is no coming back!</div>
|
||||
|
@ -6,7 +6,7 @@
|
||||
{{ data_get($member, 'pivot.role') }}</td>
|
||||
<td>
|
||||
{{-- TODO: This is not good --}}
|
||||
@if (auth()->user()->isAdmin())
|
||||
@if (auth()->user()->isAdminFromSession())
|
||||
@if ($member->id !== auth()->user()->id)
|
||||
@if (data_get($member, 'pivot.role') !== 'owner')
|
||||
@if (data_get($member, 'pivot.role') !== 'admin')
|
||||
|
@ -11,13 +11,22 @@
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class=" group-hover:text-white">
|
||||
{{ $server->name }}
|
||||
@if (!$server->settings->is_reachable)
|
||||
<span class="text-xs text-error">not validated yet</span>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
<div class="text-xs group-hover:text-white"
|
||||
href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}">
|
||||
{{ $server->description }}</div>
|
||||
<div class="flex gap-1 text-xs text-error">
|
||||
@if (!$server->settings->is_reachable)
|
||||
<span>Not reachable</span>
|
||||
@endif
|
||||
@if (!$server->settings->is_reachable && !$server->settings->is_usable)
|
||||
&
|
||||
@endif
|
||||
@if (!$server->settings->is_usable)
|
||||
<span>Not usable by Coolify</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
</div>
|
||||
|
5
resources/views/settings/license.blade.php
Normal file
5
resources/views/settings/license.blade.php
Normal file
@ -0,0 +1,5 @@
|
||||
<x-layout-subscription>
|
||||
<x-settings.navbar />
|
||||
<h3>Resale License</h3>
|
||||
<livewire:check-license />
|
||||
</x-layout-subscription>
|
34
resources/views/subscription.blade.php
Normal file
34
resources/views/subscription.blade.php
Normal file
@ -0,0 +1,34 @@
|
||||
<x-layout-subscription>
|
||||
@if ($settings->is_resale_license_active)
|
||||
<div class="flex gap-2">
|
||||
<h3>Subscription</h3>
|
||||
<livewire:switch-team />
|
||||
</div>
|
||||
<div class="flex items-center pb-8">
|
||||
<span>Currently active team: {{ session('currentTeam.name') }}</span>
|
||||
</div>
|
||||
@if (data_get(auth()->user()->currentTeam(),
|
||||
'subscription'))
|
||||
<div>Status: {{ auth()->user()->currentTeam()->subscription->lemon_status }}</div>
|
||||
<div>Type: {{ auth()->user()->currentTeam()->subscription->lemon_variant_name }}</div>
|
||||
@if (auth()->user()->currentTeam()->subscription->lemon_status === 'cancelled')
|
||||
<div class="pb-4">Subscriptions ends at: {{ getEndDate() }}</div>
|
||||
<x-forms.button><a class="text-white" href="{{ getSubscriptionLink() }}">Subscribe
|
||||
Again</a>
|
||||
</x-forms.button>
|
||||
@else
|
||||
<div class="pb-4">Renews at: {{ getRenewDate() }}</div>
|
||||
@endif
|
||||
<x-forms.button><a class="text-white" href="{{ getPaymentLink() }}">Update Payment Details</a>
|
||||
</x-forms.button>
|
||||
@else
|
||||
<x-forms.button class="mt-4"><a class="text-white" href="{{ getSubscriptionLink() }}">Subscribe Now</a>
|
||||
</x-forms.button>
|
||||
@endif
|
||||
<x-forms.button><a class="text-white" href="https://app.lemonsqueezy.com/my-orders">Manage My
|
||||
Subscription</a>
|
||||
</x-forms.button>
|
||||
@else
|
||||
<div>Resale license is not active. Please contact your instance admin.</div>
|
||||
@endif
|
||||
</x-layout-subscription>
|
43
resources/views/team/members.blade.php
Normal file
43
resources/views/team/members.blade.php
Normal file
@ -0,0 +1,43 @@
|
||||
<x-layout>
|
||||
<x-team.navbar :team="session('currentTeam')" />
|
||||
<h3>Members</h3>
|
||||
<div class="pt-4 overflow-hidden">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (auth()->user()->currentTeam()->members->sortBy('name') as $member)
|
||||
<livewire:team.member :member="$member" :wire:key="$member->id" />
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if (auth()->user()->isAdminFromSession())
|
||||
<div class="py-4">
|
||||
@if (is_transactional_emails_active())
|
||||
<h3 class="pb-4">Invite a new member</h3>
|
||||
@else
|
||||
<h3>Invite a new member</h3>
|
||||
@if (auth()->user()->isInstanceAdmin())
|
||||
<div class="pb-4 text-xs text-warning">You need to configure <a href="/settings/emails"
|
||||
class="underline text-warning">Transactional Emails</a>
|
||||
before
|
||||
you can invite a
|
||||
new
|
||||
member
|
||||
via
|
||||
email.
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
<livewire:team.invite-link />
|
||||
</div>
|
||||
<livewire:team.invitations :invitations="$invitations" />
|
||||
@endif
|
||||
</x-layout>
|
@ -1,46 +1,31 @@
|
||||
<x-layout>
|
||||
<x-team.navbar :team="session('currentTeam')" />
|
||||
<livewire:team.form />
|
||||
<h3>Members</h3>
|
||||
<div class="pt-4 overflow-hidden">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (auth()->user()->currentTeam()->members->sortBy('name') as $member)
|
||||
<livewire:team.member :member="$member" :wire:key="$member->id" />
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if (auth()->user()->isAdmin())
|
||||
<div class="py-4">
|
||||
@if (is_transactional_emails_active())
|
||||
<h3 class="pb-4">Invite a new member</h3>
|
||||
@else
|
||||
<h3>Invite a new member</h3>
|
||||
@if (auth()->user()->isInstanceAdmin())
|
||||
<div class="pb-4 text-xs text-warning">You need to configure <a href="/settings/emails"
|
||||
class="underline text-warning">Transactional Emails</a>
|
||||
before
|
||||
you can invite a
|
||||
new
|
||||
member
|
||||
via
|
||||
email.
|
||||
</div>
|
||||
@if (isCloud())
|
||||
<div class="pb-8">
|
||||
<h3>Subscription</h3>
|
||||
@if (data_get(auth()->user()->currentTeam(),
|
||||
'subscription'))
|
||||
<div>Status: {{ auth()->user()->currentTeam()->subscription->lemon_status }}</div>
|
||||
<div>Type: {{ auth()->user()->currentTeam()->subscription->lemon_variant_name }}</div>
|
||||
@if (auth()->user()->currentTeam()->subscription->lemon_status === 'cancelled')
|
||||
<div class="pb-4">Subscriptions ends at: {{ getRenewDate() }}</div>
|
||||
<x-forms.button><a class="text-white" href="{{ getSubscriptionLink() }}">Subscribe
|
||||
Again</a>
|
||||
</x-forms.button>
|
||||
@else
|
||||
<div class="pb-4">Renews at: {{ getRenewDate() }}</div>
|
||||
@endif
|
||||
<x-forms.button><a class="text-white" href="{{ getPaymentLink() }}">Update Payment Details</a>
|
||||
</x-forms.button>
|
||||
@else
|
||||
<x-forms.button class="mt-4"><a class="text-white" href="{{ getSubscriptionLink() }}">Subscribe Now</a>
|
||||
</x-forms.button>
|
||||
@endif
|
||||
<livewire:team.invite-link />
|
||||
<x-forms.button><a class="text-white" href="https://app.lemonsqueezy.com/my-orders">Manage My
|
||||
Subscription</a>
|
||||
</x-forms.button>
|
||||
</div>
|
||||
<livewire:team.invitations :invitations="$invitations" />
|
||||
@endif
|
||||
|
||||
<livewire:team.delete />
|
||||
</x-layout>
|
||||
|
@ -84,14 +84,16 @@
|
||||
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/', [Controller::class, 'dashboard'])->name('dashboard');
|
||||
Route::get('/license', [Controller::class, 'license'])->name('license');
|
||||
Route::get('/subscription', [Controller::class, 'subscription'])->name('subscription');
|
||||
Route::get('/settings', [Controller::class, 'settings'])->name('settings.configuration');
|
||||
Route::get('/settings/emails', [Controller::class, 'emails'])->name('settings.emails');
|
||||
Route::get('/settings/license', [Controller::class, 'license'])->name('settings.license');
|
||||
Route::get('/profile', fn () => view('profile', ['request' => request()]))->name('profile');
|
||||
Route::get('/team', [Controller::class, 'team'])->name('team.show');
|
||||
Route::get('/team/new', fn () => view('team.create'))->name('team.create');
|
||||
Route::get('/team/notifications', fn () => view('team.notifications'))->name('team.notifications');
|
||||
Route::get('/command-center', fn () => view('command-center', ['servers' => Server::validated()->get()]))->name('command-center');
|
||||
Route::get('/team/members', [Controller::class, 'members'])->name('team.members');
|
||||
Route::get('/command-center', fn () => view('command-center', ['servers' => Server::isReachable()->get()]))->name('command-center');
|
||||
Route::get('/invitations/{uuid}', [Controller::class, 'acceptInvitation'])->name('team.invitation.accept');
|
||||
Route::get('/invitations/{uuid}/revoke', [Controller::class, 'revokeInvitation'])->name('team.invitation.revoke');
|
||||
});
|
||||
@ -154,7 +156,7 @@
|
||||
]);
|
||||
})->name('destination.all');
|
||||
Route::get('/destination/new', function () {
|
||||
$servers = Server::validated()->get();
|
||||
$servers = Server::isUsable()->get();
|
||||
$pre_selected_server_uuid = data_get(request()->query(), 'server');
|
||||
if ($pre_selected_server_uuid) {
|
||||
$server = $servers->firstWhere('uuid', $pre_selected_server_uuid);
|
||||
|
@ -4,6 +4,10 @@
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\Webhook;
|
||||
use App\Models\User;
|
||||
use App\Models\Team;
|
||||
use App\Models\Subscription;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
@ -170,3 +174,94 @@
|
||||
return general_error_handler(err: $e);
|
||||
}
|
||||
});
|
||||
|
||||
if (isCloud()) {
|
||||
Route::post('/payments/events', function () {
|
||||
try {
|
||||
|
||||
$secret = config('coolify.lemon_squeezy_webhook_secret');
|
||||
$payload = request()->collect();
|
||||
$hash = hash_hmac('sha256', $payload, $secret);
|
||||
$signature = request()->header('X-Signature');
|
||||
|
||||
if (!hash_equals($hash, $signature)) {
|
||||
return response('Invalid signature.', 400);
|
||||
}
|
||||
|
||||
$webhook = Webhook::create([
|
||||
'type' => 'lemonsqueezy',
|
||||
'payload' => $payload
|
||||
]);
|
||||
$event = data_get($payload, 'meta.event_name');
|
||||
ray('Subscription event: ' . $event);
|
||||
$email = data_get($payload, 'data.attributes.user_email');
|
||||
$team_id = data_get($payload, 'meta.custom_data.team_id');
|
||||
if (is_null($team_id) || empty($team_id)) {
|
||||
throw new \Exception('No team_id found in webhook payload.');
|
||||
}
|
||||
$subscription_id = data_get($payload, 'data.id');
|
||||
$order_id = data_get($payload, 'data.attributes.order_id');
|
||||
$product_id = data_get($payload, 'data.attributes.product_id');
|
||||
$variant_id = data_get($payload, 'data.attributes.variant_id');
|
||||
$variant_name = data_get($payload, 'data.attributes.variant_name');
|
||||
$customer_id = data_get($payload, 'data.attributes.customer_id');
|
||||
$status = data_get($payload, 'data.attributes.status');
|
||||
$trial_ends_at = data_get($payload, 'data.attributes.trial_ends_at');
|
||||
$renews_at = data_get($payload, 'data.attributes.renews_at');
|
||||
$ends_at = data_get($payload, 'data.attributes.ends_at');
|
||||
$update_payment_method = data_get($payload, 'data.attributes.urls.update_payment_method');
|
||||
$team = Team::find($team_id);
|
||||
$found = $team->members->where('email', $email)->first();
|
||||
if (!$found->isAdmin()) {
|
||||
throw new \Exception("User {$email} is not an admin or owner of team {$team->id}.");
|
||||
}
|
||||
switch ($event) {
|
||||
case 'subscription_created':
|
||||
case 'subscription_updated':
|
||||
case 'subscription_resumed':
|
||||
case 'subscription_unpaused':
|
||||
$subscription = Subscription::updateOrCreate([
|
||||
'team_id' => $team_id,
|
||||
], [
|
||||
'lemon_subscription_id' => $subscription_id,
|
||||
'lemon_customer_id' => $customer_id,
|
||||
'lemon_order_id' => $order_id,
|
||||
'lemon_product_id' => $product_id,
|
||||
'lemon_variant_id' => $variant_id,
|
||||
'lemon_status' => $status,
|
||||
'lemon_variant_name' => $variant_name,
|
||||
'lemon_trial_ends_at' => $trial_ends_at,
|
||||
'lemon_renews_at' => $renews_at,
|
||||
'lemon_ends_at' => $ends_at,
|
||||
'lemon_update_payment_menthod_url' => $update_payment_method,
|
||||
]);
|
||||
break;
|
||||
case 'subscription_cancelled':
|
||||
case 'subscription_paused':
|
||||
case 'subscription_expired':
|
||||
$subscription = Subscription::where('team_id', $team_id)->where('lemon_order_id', $order_id)->first();
|
||||
if ($subscription) {
|
||||
$subscription->update([
|
||||
'lemon_status' => $status,
|
||||
'lemon_trial_ends_at' => $trial_ends_at,
|
||||
'lemon_renews_at' => $renews_at,
|
||||
'lemon_ends_at' => $ends_at,
|
||||
'lemon_update_payment_menthod_url' => $update_payment_method,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
$webhook->update([
|
||||
'status' => 'success',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
$webhook->update([
|
||||
'status' => 'failed',
|
||||
'failure_reason' => $e->getMessage()
|
||||
]);
|
||||
} finally {
|
||||
return response('OK');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user