feat: cloud

This commit is contained in:
Andras Bacsai 2023-08-14 15:22:29 +02:00
parent b941f35812
commit e4279bf257
17 changed files with 189 additions and 74 deletions

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers;
use App\Models\PrivateKey;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
class ServerController extends Controller
{
use AuthorizesRequests, ValidatesRequests;
public function new_server()
{
$servers = auth()->user()->currentTeam()->servers->count();
$subscription = auth()->user()->currentTeam()->subscription->type();
$limits = config('constants.limits.server')[strtolower($subscription)];
$limit_reached = true ?? $servers >= $limits[$subscription];
return view('server.create', [
'limit_reached' => $limit_reached,
'private_keys' => PrivateKey::ownedByCurrentTeam()->get(),
]);
}
}

View File

@ -8,6 +8,7 @@
class ByIp extends Component class ByIp extends Component
{ {
public $private_keys; public $private_keys;
public $limit_reached;
public int|null $private_key_id = null; public int|null $private_key_id = null;
public $new_private_key_name; public $new_private_key_name;
public $new_private_key_description; public $new_private_key_description;

View File

@ -8,32 +8,38 @@
class SubscriptionValid class SubscriptionValid
{ {
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
if (auth()->user()) { $is_instance_admin = auth()->user()?->isInstanceAdmin();
if (is_cloud() && !isSubscribed()) {
ray('SubscriptionValid Middleware');
$allowed_paths = [ if (!auth()->user() || !is_cloud()) {
'subscription', if ($request->path() === 'subscription' && !$is_instance_admin) {
'login', return redirect('/');
'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 { } else {
if ($request->path() === 'subscription' && !auth()->user()->isInstanceAdmin()) { return $next($request);
return redirect('/'); }
} else { }
return $next($request); if (is_subscription_active() && $request->path() === 'subscription' && !$is_instance_admin) {
} return redirect('/');
}
if (is_subscription_in_grace_period()) {
return $next($request);
}
if (!is_subscription_active() && !is_subscription_in_grace_period()) {
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);
} }
} }
return $next($request); return $next($request);

View File

@ -12,4 +12,22 @@ public function team()
{ {
return $this->belongsTo(Team::class); return $this->belongsTo(Team::class);
} }
public function type()
{
$basic = explode(',', config('coolify.lemon_squeezy_basic_plan_ids'));
$pro = explode(',', config('coolify.lemon_squeezy_pro_plan_ids'));
$ultimate = explode(',', config('coolify.lemon_squeezy_ultimate_plan_ids'));
$subscription = $this->lemon_variant_id;
if (in_array($subscription, $basic)) {
return 'basic';
}
if (in_array($subscription, $pro)) {
return 'pro';
}
if (in_array($subscription, $ultimate)) {
return 'ultimate';
}
return 'unknown';
}
} }

View File

@ -43,11 +43,36 @@ function getEndDate()
return Carbon::parse(auth()->user()->currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s'); return Carbon::parse(auth()->user()->currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s');
} }
function isSubscribed() function is_subscription_active()
{ {
return $team = auth()->user()?->currentTeam();
auth()->user()?->currentTeam()?->subscription?->lemon_status === 'active' || if (!$team) {
(auth()->user()?->currentTeam()?->subscription?->lemon_ends_at && return false;
Carbon::parse(auth()->user()->currentTeam()->subscription->lemon_ends_at) > Carbon::now() }
) || auth()->user()->isInstanceAdmin(); $subscription = $team?->subscription;
if (!$subscription) {
return false;
}
$is_active = $subscription->lemon_status === 'active';
$is_instance_admin = auth()->user()->isInstanceAdmin();
ray($is_instance_admin);
return $is_active || $is_instance_admin;
}
function is_subscription_in_grace_period()
{
$team = auth()->user()?->currentTeam();
if (!$team) {
return false;
}
$subscription = $team?->subscription;
if (!$subscription) {
return false;
}
$is_instance_admin = auth()->user()->isInstanceAdmin();
$is_still_grace_period = $subscription->lemon_ends_at &&
Carbon::parse($subscription->lemon_ends_at) > Carbon::now();
return $is_still_grace_period || $is_instance_admin;
} }

View File

@ -6,4 +6,11 @@
'expiration' => 10, 'expiration' => 10,
], ],
], ],
'limits' => [
'server' => [
'basic' => 1,
'pro' => 3,
'ultimate' => 9999999999999999999,
]
]
]; ];

View File

@ -3,9 +3,16 @@
return [ return [
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),
'license_url' => 'https://license.coolify.io', 'license_url' => 'https://license.coolify.io',
'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET'), 'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', null),
'lemon_squeezy_checkout_id_monthly' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY'), 'lemon_squeezy_checkout_id_monthly_basic' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_BASIC', null),
'lemon_squeezy_checkout_id_yearly' => env('LEMON_SQUEEZY_CHECKOUT_ID_YEARLY'), 'lemon_squeezy_checkout_id_monthly_pro' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_PRO', null),
'lemon_squeezy_checkout_id_monthly_ultimate' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_ULTIMATE', null),
'lemon_squeezy_checkout_id_yearly_basic' => env('LEMON_SQUEEZY_CHECKOUT_ID_YEARLY_BASIC', null),
'lemon_squeezy_checkout_id_yearly_pro' => env('LEMON_SQUEEZY_CHECKOUT_ID_YEARLY_PRO', null),
'lemon_squeezy_checkout_id_yearly_ultimate' => env('LEMON_SQUEEZY_CHECKOUT_ID_YEARLY_ULTIMATE', null),
'lemon_squeezy_basic_plan_ids' => env('LEMON_SQUEEZY_BASIC_PLAN_IDS', ""),
'lemon_squeezy_pro_plan_ids' => env('LEMON_SQUEEZY_PRO_PLAN_IDS', ""),
'lemon_squeezy_ultimate_plan_ids' => env('LEMON_SQUEEZY_ULTIMATE_PLAN_IDS', ""),
'mux_enabled' => env('MUX_ENABLED', true), 'mux_enabled' => env('MUX_ENABLED', true),
'dev_webhook' => env('SERVEO_URL'), 'dev_webhook' => env('SERVEO_URL'),
'base_config_path' => env('BASE_CONFIG_PATH', '/_data/coolify'), 'base_config_path' => env('BASE_CONFIG_PATH', '/_data/coolify'),

View File

@ -34,8 +34,15 @@ services:
- PHP_PM_MAX_SPARE_SERVERS=10 - PHP_PM_MAX_SPARE_SERVERS=10
- SELF_HOSTED - SELF_HOSTED
- LEMON_SQUEEZY_WEBHOOK_SECRET - LEMON_SQUEEZY_WEBHOOK_SECRET
- LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY - LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_BASIC
- LEMON_SQUEEZY_CHECKOUT_ID_YEARLY - LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_PRO
- LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_ULTIMATE
- LEMON_SQUEEZY_CHECKOUT_ID_YEARLY_BASIC
- LEMON_SQUEEZY_CHECKOUT_ID_YEARLY_PRO
- LEMON_SQUEEZY_CHECKOUT_ID_YEARLY_ULTIMATE
- LEMON_SQUEEZY_BASIC_PLAN_IDS
- LEMON_SQUEEZY_PRO_PLAN_IDS
- LEMON_SQUEEZY_ULTIMATE_PLAN_IDS
ports: ports:
- "${APP_PORT:-8000}:80" - "${APP_PORT:-8000}:80"
expose: expose:

View File

@ -25,7 +25,7 @@
<body> <body>
@livewireScripts @livewireScripts
<x-toaster-hub /> <x-toaster-hub />
@if (auth()->user()->isInstanceAdmin()) @if (auth()->user()->isInstanceAdmin() || is_subscription_in_grace_period())
<div class="fixed top-3 left-4" id="vue"> <div class="fixed top-3 left-4" id="vue">
<magic-bar></magic-bar> <magic-bar></magic-bar>
</div> </div>

View File

@ -0,0 +1,6 @@
<div class="flex flex-col items-center justify-center h-screen">
<span class="text-xl font-bold text-white">You have reached the limit of {{ $name }} you can create.</span>
<span>Please <a class="text-white underline "href="{{ route('team.show') }}">upgrade your
subscription<a /> to create more
{{ $name }}.</span>
</div>

View File

@ -111,7 +111,7 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
<span x-show="selected === 'yearly'" x-cloak class="text-warning">(save $6)</span> <span x-show="selected === 'yearly'" x-cloak class="text-warning">(save $6)</span>
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic" class="buyme" <a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic" class="buyme"
href="{{ getSubscriptionLink('monthly') }}">Subscribe</a> href="{{ getSubscriptionLink('monthly_basic') }}">Subscribe</a>
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic" class="buyme" <a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic" class="buyme"
href="{{ getSubscriptionLink('yearly') }}">Subscribe</a> href="{{ getSubscriptionLink('yearly') }}">Subscribe</a>
<p class="mt-10 text-sm leading-6 text-white h-[6.5rem]">Start self-hosting in <p class="mt-10 text-sm leading-6 text-white h-[6.5rem]">Start self-hosting in
@ -185,7 +185,7 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
</p> </p>
<span x-show="selected === 'yearly'" x-cloak class="text-warning">(save $29)</span> <span x-show="selected === 'yearly'" x-cloak class="text-warning">(save $29)</span>
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-essential" class="buyme" <a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-essential" class="buyme"
href="{{ getSubscriptionLink('monthly') }}">Subscribe</a> href="{{ getSubscriptionLink('monthly_pro') }}">Subscribe</a>
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-essential" class="buyme" <a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-essential" class="buyme"
href="{{ getSubscriptionLink('yearly') }}">Subscribe</a> href="{{ getSubscriptionLink('yearly') }}">Subscribe</a>
<p class="h-20 mt-10 text-sm leading-6 text-white">Scale your business or self-hosting environment. <p class="h-20 mt-10 text-sm leading-6 text-white">Scale your business or self-hosting environment.
@ -255,7 +255,7 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
</p> </p>
<span x-show="selected === 'yearly'" x-cloak class="text-warning">(save $69)</span> <span x-show="selected === 'yearly'" x-cloak class="text-warning">(save $69)</span>
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-growth" class="buyme" <a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-growth" class="buyme"
href="{{ getSubscriptionLink('monthly') }}">Subscribe</a> href="{{ getSubscriptionLink('monthly_ultimate') }}">Subscribe</a>
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-growth" class="buyme" <a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-growth" class="buyme"
href="{{ getSubscriptionLink('yearly') }}">Subscribe</a> href="{{ getSubscriptionLink('yearly') }}">Subscribe</a>
<p class="h-20 mt-10 text-sm leading-6 text-white">Deploy complex infrastuctures and <p class="h-20 mt-10 text-sm leading-6 text-white">Deploy complex infrastuctures and

View File

@ -1,29 +1,34 @@
<div> <div>
<h1>Create a new Server</h1> @if ($limit_reached)
<div class="subtitle ">Servers are the main blocks of your infrastructure.</div> <x-limit-reached name="servers" />
<form class="flex flex-col gap-2" wire:submit.prevent='submit'> @else
<div class="flex gap-2"> <h1>Create a new Server</h1>
<x-forms.input id="name" label="Name" required /> <div class="subtitle ">Servers are the main blocks of your infrastructure.</div>
<x-forms.input id="description" label="Description" /> <form class="flex flex-col gap-2" wire:submit.prevent='submit'>
</div> <div class="flex gap-2">
<div class="flex gap-2"> <x-forms.input id="name" label="Name" required />
<x-forms.input id="ip" label="IP Address" required <x-forms.input id="description" label="Description" />
helper="Could be IP Address (127.0.0.1) or Domain Name (duckduckgo.com)." /> </div>
<x-forms.input id="user" label="User" required /> <div class="flex gap-2">
<x-forms.input type="number" id="port" label="Port" required /> <x-forms.input id="ip" label="IP Address" required
</div> helper="Could be IP Address (127.0.0.1) or Domain Name (duckduckgo.com)." />
<x-forms.select label="Private Key" id="private_key_id"> <x-forms.input id="user" label="User" required />
<option disabled>Select a private key</option> <x-forms.input type="number" id="port" label="Port" required />
@foreach ($private_keys as $key) </div>
@if ($loop->first) <x-forms.select label="Private Key" id="private_key_id">
<option selected value="{{ $key->id }}">{{ $key->name }}</option> <option disabled>Select a private key</option>
@else @foreach ($private_keys as $key)
<option value="{{ $key->id }}">{{ $key->name }}</option> @if ($loop->first)
@endif <option selected value="{{ $key->id }}">{{ $key->name }}</option>
@endforeach @else
</x-forms.select> <option value="{{ $key->id }}">{{ $key->name }}</option>
<x-forms.button type="submit"> @endif
Save New Server @endforeach
</x-forms.button> </x-forms.select>
</form> <x-forms.button type="submit">
Save New Server
</x-forms.button>
</form>
@endif
</div> </div>

View File

@ -35,6 +35,11 @@
<x-use-magic-bar link="/server/new" /> <x-use-magic-bar link="/server/new" />
</div> </div>
@endforelse @endforelse
@isset($error)
<div class="text-center text-error">
<span>{{ $error }}</span>
</div>
@endisset
<script> <script>
function goto(uuid) { function goto(uuid) {
window.location.href = '/server/' + uuid; window.location.href = '/server/' + uuid;

View File

@ -4,6 +4,6 @@
<div class="subtitle">You need to create a private key before you can create a server.</div> <div class="subtitle">You need to create a private key before you can create a server.</div>
<livewire:private-key.create from="server" /> <livewire:private-key.create from="server" />
@else @else
<livewire:server.new.by-ip :private_keys="$private_keys" /> <livewire:server.new.by-ip :private_keys="$private_keys" :limit_reached="$limit_reached" />
@endif @endif
</x-layout> </x-layout>

View File

@ -5,19 +5,25 @@
<livewire:team.form /> <livewire:team.form />
@if (is_cloud()) @if (is_cloud())
<div class="pb-8"> <div class="pb-8">
<h3>Subscription</h3> <h2>Subscription</h2>
@if (data_get(auth()->user()->currentTeam(), @if (data_get(auth()->user()->currentTeam(),
'subscription')) 'subscription'))
<div>Status: {{ auth()->user()->currentTeam()->subscription->lemon_status }}</div> <div>Status: {{ auth()->user()->currentTeam()->subscription->lemon_status }}</div>
<div>Type: {{ auth()->user()->currentTeam()->subscription->lemon_variant_name }}</div> <div>Type: {{ auth()->user()->currentTeam()->subscription->lemon_variant_name }}</div>
@if (auth()->user()->currentTeam()->subscription->lemon_status === 'cancelled') @if (auth()->user()->currentTeam()->subscription->lemon_status === 'cancelled')
<div class="pb-4">Subscriptions ends at: {{ getRenewDate() }}</div> <div class="pb-4">Subscriptions ends at: {{ getRenewDate() }}</div>
<x-forms.button><a class="text-white" href="{{ route('subscription') }}">Subscribe <x-forms.button class="bg-coollabs-gradient"><a class="text-white hover:no-underline"
Again</a> href="{{ route('subscription') }}">Resume Subscription</a>
</x-forms.button> </x-forms.button>
<div class="py-4">If you would like to change the subscription to a lower/higher plan, <a
class="text-white underline" href="https://docs.coollabs.io/contact" target="_blank">please
contact
us.</a></div>
@else @else
<div class="pb-4">Renews at: {{ getRenewDate() }}</div> <div class="pb-4">Renews at: {{ getRenewDate() }}</div>
@endif @endif
<x-forms.button><a class="text-white hover:no-underline" href="{{ getPaymentLink() }}">Update Payment <x-forms.button><a class="text-white hover:no-underline" href="{{ getPaymentLink() }}">Update Payment
Details</a> Details</a>
</x-forms.button> </x-forms.button>

View File

@ -4,9 +4,8 @@
->currentTeam()" /> ->currentTeam()" />
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<h2 class="pb-4">S3 Storages</h2> <h2 class="pb-4">S3 Storages</h2>
<x-forms.button class="btn"> <a class="text-white hover:no-underline" href="/team/storages/new"> <x-forms.button class="btn">+ Add
<a class="text-white hover:no-underline" href="/team/storages/new">+ Add</a> </x-forms.button></a>
</x-forms.button>
</div> </div>
<div class="grid gap-2 lg:grid-cols-2"> <div class="grid gap-2 lg:grid-cols-2">
@forelse ($s3 as $storage) @forelse ($s3 as $storage)

View File

@ -5,6 +5,7 @@
use App\Http\Controllers\DatabaseController; use App\Http\Controllers\DatabaseController;
use App\Http\Controllers\MagicController; use App\Http\Controllers\MagicController;
use App\Http\Controllers\ProjectController; use App\Http\Controllers\ProjectController;
use App\Http\Controllers\ServerController;
use App\Models\GithubApp; use App\Models\GithubApp;
use App\Models\GitlabApp; use App\Models\GitlabApp;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
@ -71,9 +72,7 @@
Route::get('/servers', fn () => view('server.all', [ Route::get('/servers', fn () => view('server.all', [
'servers' => Server::ownedByCurrentTeam()->get() 'servers' => Server::ownedByCurrentTeam()->get()
]))->name('server.all'); ]))->name('server.all');
Route::get('/server/new', fn () => view('server.create', [ Route::get('/server/new', [ServerController::class, 'new_server'])->name('server.create');
'private_keys' => PrivateKey::ownedByCurrentTeam()->get(),
]))->name('server.create');
Route::get('/server/{server_uuid}', fn () => view('server.show', [ Route::get('/server/{server_uuid}', fn () => view('server.show', [
'server' => Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail(), 'server' => Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail(),
]))->name('server.show'); ]))->name('server.show');