diff --git a/app/Console/Commands/InviteFromWaitlist.php b/app/Console/Commands/InviteFromWaitlist.php
new file mode 100644
index 000000000..367e7d3e7
--- /dev/null
+++ b/app/Console/Commands/InviteFromWaitlist.php
@@ -0,0 +1,77 @@
+next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first();
+ if ($this->next_patient) {
+ $this->register_user();
+ $this->remove_from_waitlist();
+ $this->send_email();
+ } else {
+ $this->info('No one in the waitlist who is verified. 👀');
+ }
+ }
+ private function register_user()
+ {
+ $already_registered = User::whereEmail($this->next_patient->email)->first();
+ if (!$already_registered) {
+ $this->password = Str::password();
+ $this->new_user = User::create([
+ 'name' => Str::of($this->next_patient->email)->before('@'),
+ 'email' => $this->next_patient->email,
+ 'password' => Hash::make($this->password),
+ 'force_password_reset' => true,
+ ]);
+ $this->info("User registered ({$this->next_patient->email}) successfully. 🎉");
+ } else {
+ throw new \Exception('User already registered');
+ }
+ }
+ private function remove_from_waitlist()
+ {
+ $this->next_patient->delete();
+ $this->info("User removed from waitlist successfully.");
+ }
+ private function send_email()
+ {
+ $mail = new MailMessage();
+ $mail->view('emails.waitlist-invitation', [
+ 'email' => $this->next_patient->email,
+ 'password' => $this->password,
+ ]);
+ $mail->subject('Congratulations! You are invited to join Coolify Cloud.');
+ send_user_an_email($mail, $this->next_patient->email);
+ $this->info("Email sent successfully. 📧");
+ }
+}
diff --git a/app/Http/Livewire/Subscription/Actions.php b/app/Http/Livewire/Subscription/Actions.php
new file mode 100644
index 000000000..588a0521c
--- /dev/null
+++ b/app/Http/Livewire/Subscription/Actions.php
@@ -0,0 +1,72 @@
+user()->currentTeam()->subscription->lemon_subscription_id;
+ if (!$subscription_id) {
+ throw new \Exception('No subscription found');
+ }
+ $response = Http::withHeaders([
+ 'Accept' => 'application/vnd.api+json',
+ 'Content-Type' => 'application/vnd.api+json',
+ 'Authorization' => 'Bearer ' . config('coolify.lemon_squeezy_api_key'),
+ ])->delete('https://api.lemonsqueezy.com/v1/subscriptions/' . $subscription_id);
+ $json = $response->json();
+ if ($response->failed()) {
+ $error = data_get($json, 'errors.0.status');
+ if ($error === '404') {
+ throw new \Exception('Subscription not found.');
+ }
+ throw new \Exception(data_get($json, 'errors.0.title', 'Something went wrong. Please try again later.'));
+ } else {
+ $this->emit('success', 'Subscription cancelled successfully. Reloading in 5s.');
+ $this->emit('reloadWindow', 5000);
+ }
+ } catch (\Exception $e) {
+ return general_error_handler($e, $this);
+ }
+ }
+ public function resume()
+ {
+ try {
+ $subscription_id = auth()->user()->currentTeam()->subscription->lemon_subscription_id;
+ if (!$subscription_id) {
+ throw new \Exception('No subscription found');
+ }
+ $response = Http::withHeaders([
+ 'Accept' => 'application/vnd.api+json',
+ 'Content-Type' => 'application/vnd.api+json',
+ 'Authorization' => 'Bearer ' . config('coolify.lemon_squeezy_api_key'),
+ ])->patch('https://api.lemonsqueezy.com/v1/subscriptions/' . $subscription_id, [
+ 'data' => [
+ 'type' => 'subscriptions',
+ 'id' => $subscription_id,
+ 'attributes' => [
+ 'cancelled' => false,
+ ],
+ ],
+ ]);
+ $json = $response->json();
+ if ($response->failed()) {
+ $error = data_get($json, 'errors.0.status');
+ if ($error === '404') {
+ throw new \Exception('Subscription not found.');
+ }
+ throw new \Exception(data_get($json, 'errors.0.title', 'Something went wrong. Please try again later.'));
+ } else {
+ $this->emit('success', 'Subscription resumed successfully. Reloading in 5s.');
+ $this->emit('reloadWindow', 5000);
+ }
+ } catch (\Exception $e) {
+ return general_error_handler($e, $this);
+ }
+ }
+}
diff --git a/app/Http/Livewire/Waitlist.php b/app/Http/Livewire/Waitlist.php
index ffa1b1f5c..de633ceac 100644
--- a/app/Http/Livewire/Waitlist.php
+++ b/app/Http/Livewire/Waitlist.php
@@ -18,7 +18,7 @@ class Waitlist extends Component
public function mount()
{
if (is_dev()) {
- $this->email = 'test@example.com';
+ $this->email = 'waitlist@example.com';
}
}
public function submit()
@@ -27,8 +27,7 @@ public function submit()
try {
$already_registered = User::whereEmail($this->email)->first();
if ($already_registered) {
- $this->emit('success', 'You are already registered (Thank you 💜).');
- return;
+ throw new \Exception('You are already on the waitlist or registered.
Please check your email to verify your email address or contact support.');
}
$found = ModelsWaitlist::where('email', $this->email)->first();
if ($found) {
@@ -36,7 +35,7 @@ public function submit()
$this->emit('error', 'You are already on the waitlist.
Please check your email to verify your email address.');
return;
}
- $this->emit('error', 'You are already on the waitlist.');
+ $this->emit('error', 'You are already on the waitlist.
You will be notified when your turn comes.
Thank you.');
return;
}
$waitlist = ModelsWaitlist::create([
diff --git a/app/Jobs/SendConfirmationForWaitlistJob.php b/app/Jobs/SendConfirmationForWaitlistJob.php
index 5c1518fc6..3ff4982d9 100755
--- a/app/Jobs/SendConfirmationForWaitlistJob.php
+++ b/app/Jobs/SendConfirmationForWaitlistJob.php
@@ -24,10 +24,6 @@ public function __construct(public string $email, public string $uuid)
public function handle()
{
try {
- $settings = InstanceSettings::get();
-
-
- set_transanctional_email_settings($settings);
$mail = new MailMessage();
$confirmation_url = base_url() . '/webhooks/waitlist/confirm?email=' . $this->email . '&confirmation_code=' . $this->uuid;
@@ -39,18 +35,7 @@ public function handle()
'cancel_url' => $cancel_url,
]);
$mail->subject('You are on the waitlist!');
- Mail::send(
- [],
- [],
- fn(Message $message) => $message
- ->from(
- data_get($settings, 'smtp_from_address'),
- data_get($settings, 'smtp_from_name')
- )
- ->to($this->email)
- ->subject($mail->subject)
- ->html((string) $mail->render())
- );
+ send_user_an_email($mail, $this->email);
} catch (\Throwable $th) {
send_internal_notification('SendConfirmationForWaitlistJob failed with error: ' . $th->getMessage());
ray($th->getMessage());
diff --git a/app/Models/User.php b/app/Models/User.php
index f0a85d182..b048ef9e6 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -4,7 +4,6 @@
use App\Notifications\Channels\SendsEmail;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
-use App\Notifications\TrnsactionalEmails\ResetPassword;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 2d315020f..4b470cb48 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -5,7 +5,10 @@
use App\Notifications\Internal\GeneralNotification;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Illuminate\Database\QueryException;
+use Illuminate\Mail\Message;
+use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use Nubs\RandomNameGenerator\All;
@@ -205,3 +208,20 @@ function send_internal_notification(string $message): void
ray($th->getMessage());
}
}
+function send_user_an_email(MailMessage $mail, string $email): void
+{
+ $settings = InstanceSettings::get();
+ set_transanctional_email_settings($settings);
+ Mail::send(
+ [],
+ [],
+ fn (Message $message) => $message
+ ->from(
+ data_get($settings, 'smtp_from_address'),
+ data_get($settings, 'smtp_from_name')
+ )
+ ->to($email)
+ ->subject($mail->subject)
+ ->html((string) $mail->render())
+ );
+}
diff --git a/config/coolify.php b/config/coolify.php
index b9e092175..0276b9a78 100644
--- a/config/coolify.php
+++ b/config/coolify.php
@@ -4,6 +4,7 @@
'self_hosted' => env('SELF_HOSTED', true),
'waitlist' => env('WAITLIST', false),
'license_url' => 'https://license.coolify.io',
+ 'lemon_squeezy_api_key' => env('LEMON_SQUEEZY_API_KEY', null),
'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', null),
'lemon_squeezy_checkout_id_monthly_basic' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_BASIC', null),
'lemon_squeezy_checkout_id_monthly_pro' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_PRO', null),
diff --git a/resources/js/components/MagicBar.vue b/resources/js/components/MagicBar.vue
index 0626f16f1..f0203b885 100644
--- a/resources/js/components/MagicBar.vue
+++ b/resources/js/components/MagicBar.vue
@@ -128,6 +128,15 @@
+
+
+
+
+
+
+
@@ -273,6 +282,14 @@ const magicActions = [{
},
{
id: 4,
+ name: 'Deploy: Dockerfile',
+ tags: 'dockerfile,deploy',
+ icon: 'destination',
+ new: true,
+ sequence: ['main', 'server', 'destination', 'project', 'environment', 'redirect']
+},
+{
+ id: 5,
name: 'Create: Server',
tags: 'server,ssh,new,create',
icon: 'server',
@@ -280,7 +297,7 @@ const magicActions = [{
sequence: ['main', 'redirect']
},
{
- id: 5,
+ id: 6,
name: 'Create: Source',
tags: 'source,git,gitlab,github,bitbucket,gitea,new,create',
icon: 'git',
@@ -288,7 +305,7 @@ const magicActions = [{
sequence: ['main', 'redirect']
},
{
- id: 6,
+ id: 7,
name: 'Create: Private Key',
tags: 'private,key,ssh,new,create',
icon: 'key',
@@ -296,16 +313,15 @@ const magicActions = [{
sequence: ['main', 'redirect']
},
{
- id: 7,
+ id: 8,
name: 'Create: Destination',
tags: 'destination,docker,network,new,create',
icon: 'destination',
new: true,
sequence: ['main', 'server', 'redirect']
},
-
{
- id: 8,
+ id: 9,
name: 'Create: Team',
tags: 'team,member,new,create',
icon: 'team',
@@ -313,74 +329,82 @@ const magicActions = [{
sequence: ['main', 'redirect']
},
{
- id: 9,
+ id: 10,
+ name: 'Create: S3 Storage',
+ tags: 's3,storage,new,create',
+ icon: 'storage',
+ new: true,
+ sequence: ['main', 'redirect']
+},
+{
+ id: 11,
name: 'Goto: Dashboard',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
- id: 10,
+ id: 12,
name: 'Goto: Servers',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
- id: 11,
+ id: 13,
name: 'Goto: Private Keys',
tags: 'destination,docker,network,new,create,ssh,private,key',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
- id: 12,
+ id: 14,
name: 'Goto: Projects',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
- id: 13,
+ id: 15,
name: 'Goto: Sources',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
- id: 14,
+ id: 16,
name: 'Goto: Destinations',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
- id: 15,
+ id: 17,
name: 'Goto: Settings',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
- id: 16,
+ id: 18,
name: 'Goto: Command Center',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
- id: 17,
+ id: 19,
name: 'Goto: Notifications',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
- id: 18,
+ id: 20,
name: 'Goto: Profile',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
- id: 19,
+ id: 21,
name: 'Goto: Teams',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
- id: 20,
+ id: 22,
name: 'Goto: Switch Teams',
icon: 'goto',
sequence: ['main', 'redirect']
@@ -552,55 +576,63 @@ async function redirect() {
targetUrl.searchParams.append('destination', destination)
break;
case 4:
- targetUrl.pathname = `/server/new`
+ targetUrl.pathname = `/project/${project}/${environment}/new`
+ targetUrl.searchParams.append('type', 'dockerfile')
+ targetUrl.searchParams.append('destination', destination)
break;
case 5:
- targetUrl.pathname = `/source/new`
+ targetUrl.pathname = `/server/new`
break;
case 6:
- targetUrl.pathname = `/private-key/new`
+ targetUrl.pathname = `/source/new`
break;
case 7:
+ targetUrl.pathname = `/private-key/new`
+ break;
+ case 8:
targetUrl.pathname = `/destination/new`
targetUrl.searchParams.append('server', server)
break;
- case 8:
+ case 9:
targetUrl.pathname = `/team/new`
break;
- case 9:
- targetUrl.pathname = `/`
- break;
case 10:
- targetUrl.pathname = `/servers`
+ targetUrl.pathname = `/team/storages/new`
break;
case 11:
- targetUrl.pathname = `/private-keys`
+ targetUrl.pathname = `/`
break;
case 12:
- targetUrl.pathname = `/projects`
+ targetUrl.pathname = `/servers`
break;
case 13:
- targetUrl.pathname = `/sources`
+ targetUrl.pathname = `/private-keys`
break;
case 14:
- targetUrl.pathname = `/destinations`
+ targetUrl.pathname = `/projects`
break;
case 15:
- targetUrl.pathname = `/settings`
+ targetUrl.pathname = `/sources`
break;
case 16:
- targetUrl.pathname = `/command-center`
+ targetUrl.pathname = `/destinations`
break;
case 17:
- targetUrl.pathname = `/team/notifications`
+ targetUrl.pathname = `/settings`
break;
case 18:
- targetUrl.pathname = `/profile`
+ targetUrl.pathname = `/command-center`
break;
case 19:
- targetUrl.pathname = `/team`
+ targetUrl.pathname = `/team/notifications`
break;
case 20:
+ targetUrl.pathname = `/profile`
+ break;
+ case 21:
+ targetUrl.pathname = `/team`
+ break;
+ case 22:
targetUrl.pathname = `/team`
break;
}
diff --git a/resources/views/components/layout-subscription.blade.php b/resources/views/components/layout-subscription.blade.php
index 3ee4d7513..f5103c863 100644
--- a/resources/views/components/layout-subscription.blade.php
+++ b/resources/views/components/layout-subscription.blade.php
@@ -58,8 +58,15 @@ function changePasswordFieldType(event) {
}
}
- Livewire.on('reloadWindow', () => {
- window.location.reload();
+ Livewire.on('reloadWindow', (timeout) => {
+ if (timeout) {
+ setTimeout(() => {
+ window.location.reload();
+ }, timeout);
+ return;
+ } else {
+ window.location.reload();
+ }
})