diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index f26b2ef4d..21c4de1d0 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -4,6 +4,7 @@ namespace App\Console;
use App\Jobs\CheckResaleLicenseJob;
use App\Jobs\CheckResaleLicenseKeys;
+use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\InstanceApplicationsStatusJob;
@@ -22,12 +23,14 @@ class Kernel extends ConsoleKernel
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new InstanceApplicationsStatusJob)->everyMinute();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
+ $schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
// $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 CleanupInstanceStuffsJob)->everyMinute();
$schedule->job(new InstanceApplicationsStatusJob)->everyMinute();
$schedule->job(new CheckResaleLicenseJob)->hourly();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index 9c39b3a03..84da5de2e 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -9,6 +9,7 @@ use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\TeamInvitation;
use App\Models\User;
+use App\Models\Waitlist;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
@@ -18,6 +19,12 @@ class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
+ public function waitlist() {
+ $waiting_in_line = Waitlist::whereVerified(true)->count();
+ return view('auth.waitlist', [
+ 'waiting_in_line' => $waiting_in_line,
+ ]);
+ }
public function subscription()
{
if (!is_cloud()) {
@@ -38,6 +45,9 @@ class Controller extends BaseController
]);
}
+ public function force_passoword_reset() {
+ return view('auth.force-password-reset');
+ }
public function dashboard()
{
$projects = Project::ownedByCurrentTeam()->get();
diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
index 06777812f..57b5edfec 100644
--- a/app/Http/Kernel.php
+++ b/app/Http/Kernel.php
@@ -37,6 +37,7 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
+ \App\Http\Middleware\CheckForcePasswordReset::class,
\App\Http\Middleware\SubscriptionValid::class,
],
diff --git a/app/Http/Livewire/ForcePasswordReset.php b/app/Http/Livewire/ForcePasswordReset.php
new file mode 100644
index 000000000..effceaa0f
--- /dev/null
+++ b/app/Http/Livewire/ForcePasswordReset.php
@@ -0,0 +1,36 @@
+ 'required|email',
+ 'password' => 'required|min:8',
+ 'password_confirmation' => 'required|same:password',
+ ];
+ public function mount() {
+ $this->email = auth()->user()->email;
+ }
+ public function submit() {
+ try {
+ $this->validate();
+ auth()->user()->forceFill([
+ 'password' => Hash::make($this->password),
+ 'force_password_reset' => false,
+ ])->save();
+ auth()->logout();
+ return redirect()->route('login')->with('status', 'Your initial password has been set.');
+ } catch(\Exception $e) {
+ return general_error_handler(err:$e, that:$this);
+ }
+ }
+
+}
diff --git a/app/Http/Livewire/Waitlist.php b/app/Http/Livewire/Waitlist.php
new file mode 100644
index 000000000..7f26351e1
--- /dev/null
+++ b/app/Http/Livewire/Waitlist.php
@@ -0,0 +1,49 @@
+ 'required|email',
+ ];
+ public function mount()
+ {
+ if (is_dev()) {
+ $this->email = 'test@example.com';
+ }
+ }
+ public function submit()
+ {
+ $this->validate();
+ try {
+ $found = ModelsWaitlist::where('email', $this->email)->first();
+ ray($found);
+ if ($found) {
+ if (!$found->verified) {
+ $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.');
+ return;
+ }
+ $waitlist = ModelsWaitlist::create([
+ 'email' => $this->email,
+ 'type' => 'registration',
+ ]);
+
+ $this->emit('success', 'You have been added to the waitlist.');
+ dispatch(new SendConfirmationForWaitlistJob($this->email, $waitlist->uuid));
+ } catch (\Exception $e) {
+ return general_error_handler(err: $e, that: $this);
+ }
+
+ }
+}
diff --git a/app/Http/Middleware/CheckForcePasswordReset.php b/app/Http/Middleware/CheckForcePasswordReset.php
new file mode 100644
index 000000000..8d1670de5
--- /dev/null
+++ b/app/Http/Middleware/CheckForcePasswordReset.php
@@ -0,0 +1,29 @@
+user()) {
+ $force_password_reset = auth()->user()->force_password_reset;
+ if ($force_password_reset) {
+ if ($request->routeIs('auth.force-password-reset') || $request->path() === 'livewire/message/force-password-reset') {
+ return $next($request);
+ }
+ return redirect()->route('auth.force-password-reset');
+ }
+ }
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php
index dae8398b7..3f17e6def 100644
--- a/app/Http/Middleware/RedirectIfAuthenticated.php
+++ b/app/Http/Middleware/RedirectIfAuthenticated.php
@@ -24,7 +24,6 @@ class RedirectIfAuthenticated
return redirect(RouteServiceProvider::HOME);
}
}
-
return $next($request);
}
}
diff --git a/app/Http/Middleware/SubscriptionValid.php b/app/Http/Middleware/SubscriptionValid.php
index 4c9d7b940..f84e6eede 100644
--- a/app/Http/Middleware/SubscriptionValid.php
+++ b/app/Http/Middleware/SubscriptionValid.php
@@ -10,7 +10,6 @@ class SubscriptionValid
{
public function handle(Request $request, Closure $next): Response
{
-
if (!auth()->user() || !is_cloud()) {
if ($request->path() === 'subscription') {
return redirect('/');
@@ -36,7 +35,10 @@ class SubscriptionValid
'subscription',
'login',
'register',
+ 'waitlist',
+ 'force-password-reset',
'logout',
+ 'livewire/message/force-password-reset',
'livewire/message/check-license',
'livewire/message/switch-team',
];
diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php
new file mode 100644
index 000000000..7b3be1a44
--- /dev/null
+++ b/app/Jobs/CleanupInstanceStuffsJob.php
@@ -0,0 +1,43 @@
+container_name;
+ // }
+
+ public function handle(): void
+ {
+ try {
+ $this->cleanup_waitlist();
+ } catch (\Exception $e) {
+ ray($e->getMessage());
+ }
+ }
+
+ private function cleanup_waitlist()
+ {
+ $waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.confirmation_valid_for_minutes')))->get();
+ foreach ($waitlist as $item) {
+ $item->delete();
+ }
+ }
+}
diff --git a/app/Jobs/SendConfirmationForWaitlistJob.php b/app/Jobs/SendConfirmationForWaitlistJob.php
new file mode 100755
index 000000000..31098e180
--- /dev/null
+++ b/app/Jobs/SendConfirmationForWaitlistJob.php
@@ -0,0 +1,59 @@
+email . '&confirmation_code=' . $this->uuid;
+ $cancel_url = base_url() . '/webhooks/waitlist/cancel?email=' . $this->email . '&confirmation_code=' . $this->uuid;
+
+ $mail->view('emails.waitlist-confirmation',
+ [
+ 'confirmation_url' => $confirmation_url,
+ '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())
+ );
+ } catch (\Throwable $th) {
+ ray($th->getMessage());
+ throw $th;
+ }
+ }
+}
diff --git a/app/Models/User.php b/app/Models/User.php
index b37a070c3..f0a85d182 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -3,6 +3,7 @@
namespace App\Models;
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;
@@ -14,18 +15,14 @@ class User extends Authenticatable implements SendsEmail
{
use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
- protected $fillable = [
- 'id',
- 'name',
- 'email',
- 'password',
- ];
+ protected $guarded = [];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
+ 'force_password_reset' => 'boolean',
];
protected static function boot()
@@ -57,7 +54,7 @@ class User extends Authenticatable implements SendsEmail
public function sendPasswordResetNotification($token): void
{
- $this->notify(new ResetPassword($token));
+ $this->notify(new TransactionalEmailsResetPassword($token));
}
public function isAdmin()
diff --git a/app/Models/Waitlist.php b/app/Models/Waitlist.php
new file mode 100644
index 000000000..552c25eb3
--- /dev/null
+++ b/app/Models/Waitlist.php
@@ -0,0 +1,11 @@
+is_registration_enabled) {
return redirect()->route('login');
}
- return view('auth.register');
+ if (config('coolify.waitlist')) {
+ return view('auth.waitlist');
+ } else {
+ return view('auth.register');
+ }
});
Fortify::loginView(function () {
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 14b6ed75a..b2d279bac 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -101,9 +101,11 @@ function is_transactional_emails_active(): bool
return data_get(InstanceSettings::get(), 'smtp_enabled');
}
-function set_transanctional_email_settings(): void
+function set_transanctional_email_settings(InstanceSettings|null $settings = null): void
{
- $settings = InstanceSettings::get();
+ if (!$settings) {
+ $settings = InstanceSettings::get();
+ }
$password = data_get($settings, 'smtp_password');
if (isset($password)) {
$password = decrypt($password);
diff --git a/config/constants.php b/config/constants.php
index 13097375e..6cb455265 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -1,5 +1,8 @@
[
+ 'confirmation_valid_for_minutes' => 10,
+ ],
'invitation' => [
'link' => [
'base_url' => '/invitations/',
@@ -11,6 +14,6 @@ return [
'basic' => 1,
'pro' => 3,
'ultimate' => 9999999999999999999,
- ]
- ]
+ ],
+ ],
];
diff --git a/config/coolify.php b/config/coolify.php
index a933d12b4..0d4c50cb1 100644
--- a/config/coolify.php
+++ b/config/coolify.php
@@ -2,6 +2,7 @@
return [
'self_hosted' => env('SELF_HOSTED', true),
+ 'waitlist' => env('WAITLIST', false),
'license_url' => 'https://license.coolify.io',
'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', null),
'lemon_squeezy_checkout_id_monthly_basic' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_BASIC', null),
diff --git a/database/migrations/2023_08_15_095902_create_waitlists_table.php b/database/migrations/2023_08_15_095902_create_waitlists_table.php
new file mode 100644
index 000000000..641d152d1
--- /dev/null
+++ b/database/migrations/2023_08_15_095902_create_waitlists_table.php
@@ -0,0 +1,31 @@
+id();
+ $table->string('uuid');
+ $table->string('type');
+ $table->string('email')->unique();
+ $table->boolean('verified')->default(false);
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('waitlists');
+ }
+};
diff --git a/database/migrations/2023_08_15_111125_update_users_table.php b/database/migrations/2023_08_15_111125_update_users_table.php
new file mode 100644
index 000000000..4453d39d4
--- /dev/null
+++ b/database/migrations/2023_08_15_111125_update_users_table.php
@@ -0,0 +1,28 @@
+boolean('force_password_reset')->default(false);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn('force_password_reset');
+ });
+ }
+};
diff --git a/database/seeders/WaitlistSeeder.php b/database/seeders/WaitlistSeeder.php
new file mode 100644
index 000000000..ce259253e
--- /dev/null
+++ b/database/seeders/WaitlistSeeder.php
@@ -0,0 +1,17 @@
+
+