feat: force password reset + waitlist

This commit is contained in:
Andras Bacsai 2023-08-15 14:11:38 +02:00
parent 952d335789
commit 88b3005589
35 changed files with 482 additions and 44 deletions

View File

@ -4,6 +4,7 @@
use App\Jobs\CheckResaleLicenseJob; use App\Jobs\CheckResaleLicenseJob;
use App\Jobs\CheckResaleLicenseKeys; use App\Jobs\CheckResaleLicenseKeys;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\DatabaseBackupJob; use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob; use App\Jobs\DockerCleanupJob;
use App\Jobs\InstanceApplicationsStatusJob; use App\Jobs\InstanceApplicationsStatusJob;
@ -22,12 +23,14 @@ protected function schedule(Schedule $schedule): void
$schedule->command('horizon:snapshot')->everyMinute(); $schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new InstanceApplicationsStatusJob)->everyMinute(); $schedule->job(new InstanceApplicationsStatusJob)->everyMinute();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes(); $schedule->job(new ProxyCheckJob)->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
// $schedule->job(new CheckResaleLicenseJob)->hourly(); // $schedule->job(new CheckResaleLicenseJob)->hourly();
// $schedule->job(new DockerCleanupJob)->everyOddHour(); // $schedule->job(new DockerCleanupJob)->everyOddHour();
// $schedule->job(new InstanceAutoUpdateJob(true))->everyMinute(); // $schedule->job(new InstanceAutoUpdateJob(true))->everyMinute();
} else { } else {
$schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
$schedule->job(new InstanceApplicationsStatusJob)->everyMinute(); $schedule->job(new InstanceApplicationsStatusJob)->everyMinute();
$schedule->job(new CheckResaleLicenseJob)->hourly(); $schedule->job(new CheckResaleLicenseJob)->hourly();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes(); $schedule->job(new ProxyCheckJob)->everyFiveMinutes();

View File

@ -9,6 +9,7 @@
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\TeamInvitation; use App\Models\TeamInvitation;
use App\Models\User; use App\Models\User;
use App\Models\Waitlist;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController; use Illuminate\Routing\Controller as BaseController;
@ -18,6 +19,12 @@ class Controller extends BaseController
{ {
use AuthorizesRequests, ValidatesRequests; 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() public function subscription()
{ {
if (!is_cloud()) { if (!is_cloud()) {
@ -38,6 +45,9 @@ public function license()
]); ]);
} }
public function force_passoword_reset() {
return view('auth.force-password-reset');
}
public function dashboard() public function dashboard()
{ {
$projects = Project::ownedByCurrentTeam()->get(); $projects = Project::ownedByCurrentTeam()->get();

View File

@ -37,6 +37,7 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class, \App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\CheckForcePasswordReset::class,
\App\Http\Middleware\SubscriptionValid::class, \App\Http\Middleware\SubscriptionValid::class,
], ],

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Livewire;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class ForcePasswordReset extends Component
{
public string $email;
public string $password;
public string $password_confirmation;
protected $rules = [
'email' => '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);
}
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Livewire;
use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\Waitlist as ModelsWaitlist;
use Livewire\Component;
class Waitlist extends Component
{
public string $email;
public int $waiting_in_line = 0;
protected $rules = [
'email' => '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. <br>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);
}
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckForcePasswordReset
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (auth()->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);
}
}

View File

@ -24,7 +24,6 @@ public function handle(Request $request, Closure $next, string ...$guards): Resp
return redirect(RouteServiceProvider::HOME); return redirect(RouteServiceProvider::HOME);
} }
} }
return $next($request); return $next($request);
} }
} }

View File

@ -10,7 +10,6 @@ class SubscriptionValid
{ {
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
if (!auth()->user() || !is_cloud()) { if (!auth()->user() || !is_cloud()) {
if ($request->path() === 'subscription') { if ($request->path() === 'subscription') {
return redirect('/'); return redirect('/');
@ -36,7 +35,10 @@ public function handle(Request $request, Closure $next): Response
'subscription', 'subscription',
'login', 'login',
'register', 'register',
'waitlist',
'force-password-reset',
'logout', 'logout',
'livewire/message/force-password-reset',
'livewire/message/check-license', 'livewire/message/check-license',
'livewire/message/switch-team', 'livewire/message/switch-team',
]; ];

View File

@ -0,0 +1,43 @@
<?php
namespace App\Jobs;
use App\Models\Waitlist;
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;
class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct()
{
}
// public function uniqueId(): string
// {
// return $this->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();
}
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Jobs;
use App\Models\InstanceSettings;
use App\Models\Waitlist;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendConfirmationForWaitlistJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
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;
$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;
}
}
}

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use App\Notifications\Channels\SendsEmail; use App\Notifications\Channels\SendsEmail;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
use App\Notifications\TrnsactionalEmails\ResetPassword; use App\Notifications\TrnsactionalEmails\ResetPassword;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
@ -14,18 +15,14 @@ class User extends Authenticatable implements SendsEmail
{ {
use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable; use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
protected $fillable = [ protected $guarded = [];
'id',
'name',
'email',
'password',
];
protected $hidden = [ protected $hidden = [
'password', 'password',
'remember_token', 'remember_token',
]; ];
protected $casts = [ protected $casts = [
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',
'force_password_reset' => 'boolean',
]; ];
protected static function boot() protected static function boot()
@ -57,7 +54,7 @@ public function getRecepients($notification)
public function sendPasswordResetNotification($token): void public function sendPasswordResetNotification($token): void
{ {
$this->notify(new ResetPassword($token)); $this->notify(new TransactionalEmailsResetPassword($token));
} }
public function isAdmin() public function isAdmin()

11
app/Models/Waitlist.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Waitlist extends BaseModel
{
use HasFactory;
protected $guarded = [];
}

View File

@ -14,6 +14,7 @@
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Contracts\RegisterResponse; use Laravel\Fortify\Contracts\RegisterResponse;
use Laravel\Fortify\Features;
use Laravel\Fortify\Fortify; use Laravel\Fortify\Fortify;
class FortifyServiceProvider extends ServiceProvider class FortifyServiceProvider extends ServiceProvider
@ -41,13 +42,19 @@ public function toResponse($request)
*/ */
public function boot(): void public function boot(): void
{ {
Fortify::createUsersUsing(CreateNewUser::class); Fortify::createUsersUsing(CreateNewUser::class);
Fortify::registerView(function () { Fortify::registerView(function () {
ray('asd');
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
if (!$settings->is_registration_enabled) { if (!$settings->is_registration_enabled) {
return redirect()->route('login'); return redirect()->route('login');
} }
return view('auth.register'); if (config('coolify.waitlist')) {
return view('auth.waitlist');
} else {
return view('auth.register');
}
}); });
Fortify::loginView(function () { Fortify::loginView(function () {

View File

@ -101,9 +101,11 @@ function is_transactional_emails_active(): bool
return data_get(InstanceSettings::get(), 'smtp_enabled'); 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'); $password = data_get($settings, 'smtp_password');
if (isset($password)) { if (isset($password)) {
$password = decrypt($password); $password = decrypt($password);

View File

@ -1,5 +1,8 @@
<?php <?php
return [ return [
'waitlist' => [
'confirmation_valid_for_minutes' => 10,
],
'invitation' => [ 'invitation' => [
'link' => [ 'link' => [
'base_url' => '/invitations/', 'base_url' => '/invitations/',
@ -11,6 +14,6 @@
'basic' => 1, 'basic' => 1,
'pro' => 3, 'pro' => 3,
'ultimate' => 9999999999999999999, 'ultimate' => 9999999999999999999,
] ],
] ],
]; ];

View File

@ -2,6 +2,7 @@
return [ return [
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),
'waitlist' => env('WAITLIST', false),
'license_url' => 'https://license.coolify.io', 'license_url' => 'https://license.coolify.io',
'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', 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_basic' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_BASIC', null),

View File

@ -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('waitlists', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,28 @@
<?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('users', function (Blueprint $table) {
$table->boolean('force_password_reset')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('force_password_reset');
});
}
};

View File

@ -0,0 +1,17 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class WaitlistSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@ -0,0 +1,3 @@
<x-layout-simple>
<livewire:force-password-reset />
</x-layout-simple>

View File

@ -8,9 +8,17 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1>{{ __('auth.login') }}</h1> <h1>{{ __('auth.login') }}</h1>
@if ($is_registration_enabled) @if ($is_registration_enabled)
<a href="/register" class="text-xs normal-case hover:no-underline btn btn-sm bg-coollabs-gradient"> @if (config('coolify.waitlist'))
{{ __('auth.register_now') }} <a href="/waitlist"
</a> class="text-xs normal-case hover:no-underline btn btn-sm bg-coollabs-gradient">
Join the waitlist
</a>
@else
<a href="/register"
class="text-xs normal-case hover:no-underline btn btn-sm bg-coollabs-gradient">
{{ __('auth.register_now') }}
</a>
@endif
@endif @endif
</div> </div>
<div> <div>

View File

@ -1,14 +1,13 @@
<x-layout-simple> <x-layout-simple>
<div class="flex items-center justify-center h-screen mx-auto"> <div class="min-h-screen hero">
<div> <div>
<div class="flex flex-col items-center pb-8"> <div class="flex flex-col items-center pb-8">
<a href="{{ route('dashboard') }}"> <a href="{{ route('dashboard') }}">
<div class="text-5xl font-bold tracking-tight text-center text-white">Coolify</div> <div class="text-5xl font-bold tracking-tight text-center text-white">Coolify</div>
</a> </a>
<x-version />
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center justify-center pb-4 text-center">
<h1>{{ __('auth.reset_password') }}</h1> <h2>{{ __('auth.reset_password') }}</h2>
</div> </div>
<div> <div>
<form action="/reset-password" method="POST" class="flex flex-col gap-2"> <form action="/reset-password" method="POST" class="flex flex-col gap-2">
@ -16,7 +15,7 @@
<input hidden id="token" name="token" value="{{ request()->route('token') }}"> <input hidden id="token" name="token" value="{{ request()->route('token') }}">
<input hidden value="{{ request()->query('email') }}" type="email" name="email" <input hidden value="{{ request()->query('email') }}" type="email" name="email"
label="{{ __('input.email') }}" /> label="{{ __('input.email') }}" />
<div class="flex gap-2"> <div class="flex flex-col gap-2">
<x-forms.input required type="password" id="password" name="password" <x-forms.input required type="password" id="password" name="password"
label="{{ __('input.password') }}" autofocus /> label="{{ __('input.password') }}" autofocus />
<x-forms.input required type="password" id="password_confirmation" name="password_confirmation" <x-forms.input required type="password" id="password_confirmation" name="password_confirmation"

View File

@ -0,0 +1,3 @@
<x-layout-simple>
<livewire:waitlist :waiting_in_line="$waiting_in_line" />
</x-layout-simple>

View File

@ -25,14 +25,15 @@ class="absolute inset-y-0 left-0 flex items-center pl-2 cursor-pointer hover:tex
<input {{ $attributes->merge(['class' => $defaultClass . ' pl-10']) }} @required($required) <input {{ $attributes->merge(['class' => $defaultClass . ' pl-10']) }} @required($required)
wire:model.defer={{ $id }} wire:dirty.class.remove='text-white' wire:model.defer={{ $id }} wire:dirty.class.remove='text-white'
wire:dirty.class="input-warning" wire:loading.attr="disabled" type="{{ $type }}" wire:dirty.class="input-warning" wire:loading.attr="disabled" type="{{ $type }}"
@readonly($readonly) @disabled($disabled) id="{{ $id }}" name="{{ $name }}"> @readonly($readonly) @disabled($disabled) id="{{ $id }}" name="{{ $name }}"
placeholder="{{ $attributes->get('placeholder') }}">
</div> </div>
@else @else
<input {{ $attributes->merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly) <input {{ $attributes->merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly)
wire:model.defer={{ $id }} wire:dirty.class.remove='text-white' wire:dirty.class="input-warning" wire:model.defer={{ $id }} wire:dirty.class.remove='text-white' wire:dirty.class="input-warning"
wire:loading.attr="disabled" type="{{ $type }}" @disabled($disabled) wire:loading.attr="disabled" type="{{ $type }}" @disabled($disabled)
id="{{ $id }}" name="{{ $name }}"> id="{{ $id }}" name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}">
@endif @endif
@if (!$label && $helper) @if (!$label && $helper)
<x-helper :helper="$helper" /> <x-helper :helper="$helper" />

View File

@ -24,11 +24,25 @@
<body> <body>
@livewireScripts @livewireScripts
<x-toaster-hub />
<main> <main>
{{ $slot }} {{ $slot }}
</main> </main>
<x-version class="fixed left-2 bottom-1" /> <x-version class="fixed left-2 bottom-1" />
<script> <script>
Livewire.on('info', (message) => {
if (message) Toaster.info(message)
})
Livewire.on('error', (message) => {
if (message) Toaster.error(message)
})
Livewire.on('warning', (message) => {
if (message) Toaster.warning(message)
})
Livewire.on('success', (message) => {
if (message) Toaster.success(message)
})
function changePasswordFieldType(event) { function changePasswordFieldType(event) {
let element = event.target let element = event.target
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
@ -47,6 +61,7 @@ function changePasswordFieldType(event) {
} }
} }
</script> </script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,8 @@
@if ($pull_request_id !== 0)
Pull Request #{{ $pull_request_id }} of {{ $name }} (<a target="_blank"
href="{{ $fqdn }}">{{ $fqdn }}</a>) deployment failed:
@else
Deployment failed of {{ $name }} (<a target="_blank" href="{{ $fqdn }}">{{ $fqdn }}</a>):
@endif
<a target="_blank" href="{{ $deployment_url }}">View Deployment Logs</a><br><br>

View File

@ -1,7 +0,0 @@
@if ($pull_request_id !== 0)
Pull Request #{{ $pull_request_id }} of {{ $name }} (<a target="_blank" href="{{ $fqdn }}">{{ $fqdn }}</a>) deployment failed:
@else
Deployment failed of {{ $name }} (<a target="_blank" href="{{ $fqdn }}">{{ $fqdn }}</a>):
@endif
<a target="_blank" href="{{ $deployment_url }}">View Deployment Logs</a><br><br>

View File

@ -0,0 +1,8 @@
@if ($pull_request_id === 0)
A new version of <a target="_blank" href="{{ $fqdn }}">{{ $fqdn }}</a> is available:
@else
Pull request #{{ $pull_request_id }} of {{ $name }} deployed successfully: <a target="_blank"
href="{{ $fqdn }}">Application Link</a> |
@endif
<a target="_blank" href="{{ $deployment_url }}">View
Deployment Logs</a><br><br>

View File

@ -1,7 +0,0 @@
@if ($pull_request_id === 0)
A new version of <a target="_blank" href="{{ $fqdn }}">{{ $fqdn }}</a> is available:
@else
Pull request #{{ $pull_request_id }} of {{ $name }} deployed successfully: <a target="_blank" href="{{ $fqdn }}">Application Link</a> |
@endif
<a target="_blank" href="{{ $deployment_url }}">View
Deployment Logs</a><br><br>

View File

@ -1,8 +1,6 @@
Hello,<br><br>
A password reset requested for your email address on "{{ config('app.name') }}".<br><br> A password reset requested for your email address on "{{ config('app.name') }}".<br><br>
Please click the following link to reset your password: <a target="_blank" href="{{ $url }}">Password Please click the following link to reset your password: <a target="_blank" href="{{ $url }}">Password
Reset</a><br><br> Reset</a><br><br>
This password reset link will expire in "{{ $count }}" minutes. This password reset link will expire in {{ $count }} minutes.

View File

@ -0,0 +1,4 @@
Someone added this email to the Coolify Cloud's waitlist.
<br>
<a href="{{ $confirmation_url }}">Click here to confirm</a>! The link will expire in {{config('constants.waitlist.confirmation_valid_for_minutes')}} minutes.<br><br>
You have no idea what <a href="https://cloud.coolify.io">Coolify Cloud</a> is or this waitlist? <a href="{{ $cancel_url }}">Click here to remove</a> you from the waitlist.

View File

@ -0,0 +1,18 @@
<div class="min-h-screen hero">
<div class="w-96 min-w-fit">
<div class="flex flex-col items-center pb-8">
<a href="{{ route('dashboard') }}">
<div class="text-5xl font-bold tracking-tight text-center text-white">Coolify</div>
</a>
</div>
<div class="flex items-center justify-center pb-4 text-center">
<h2>Set your initial password</h2>
</div>
<form class="flex flex-col gap-2" wire:submit.prevent='submit'>
<x-forms.input id="email" type="email" placeholder="Email" readonly />
<x-forms.input id="password" type="password" placeholder="New Password" />
<x-forms.input id="password_confirmation" type="password" placeholder="Confirm New Password" />
<x-forms.button type="submit">Reset Password</x-forms.button>
</form>
</div>
</div>

View File

@ -0,0 +1,23 @@
<div class="min-h-screen hero">
<div class="w-96 min-w-fit">
<div class="flex items-center justify-center pb-4 text-center">
<h2>Start self-hosting in the
<svg class="inline-block w-8 h-8 text-warning width="512" height="512" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor" fill-rule="evenodd" clip-rule="evenodd">
<path
d="M13 4h-1a4.002 4.002 0 0 0-3.874 3H8a4 4 0 1 0 0 8h8a4 4 0 0 0 .899-7.899A4.002 4.002 0 0 0 13 4Z"
opacity=".2" />
<path
d="M11 3h-1a4.002 4.002 0 0 0-3.874 3H6a4 4 0 1 0 0 8h8a4 4 0 0 0 .899-7.899A4.002 4.002 0 0 0 11 3ZM6.901 7l.193-.75A3.002 3.002 0 0 1 10 4h1c1.405 0 2.614.975 2.924 2.325l.14.61l.61.141A3.001 3.001 0 0 1 14 13H6a3 3 0 1 1 0-6h.901Z" />
</g>
</svg>
</h2>
</div>
<form class="flex items-end gap-2" wire:submit.prevent='submit'>
<x-forms.input id="email" type="email" label="Email" placeholder="youareawesome@protonmail.com" />
<x-forms.button type="submit">Join Waitlist</x-forms.button>
</form>
Waiting: {{$waiting_in_line}}
</div>
</div>

View File

@ -38,6 +38,8 @@
} }
return response()->json(['message' => 'Transactional emails are not active'], 400); return response()->json(['message' => 'Transactional emails are not active'], 400);
})->name('password.forgot'); })->name('password.forgot');
Route::get('/waitlist', [Controller::class, 'waitlist'])->name('auth.waitlist');
Route::prefix('magic')->middleware(['auth'])->group(function () { Route::prefix('magic')->middleware(['auth'])->group(function () {
Route::get('/servers', [MagicController::class, 'servers']); Route::get('/servers', [MagicController::class, 'servers']);
Route::get('/destinations', [MagicController::class, 'destinations']); Route::get('/destinations', [MagicController::class, 'destinations']);
@ -91,6 +93,7 @@
Route::middleware(['auth'])->group(function () { Route::middleware(['auth'])->group(function () {
Route::get('/', [Controller::class, 'dashboard'])->name('dashboard'); Route::get('/', [Controller::class, 'dashboard'])->name('dashboard');
Route::get('/force-password-reset', [Controller::class, 'force_passoword_reset'])->name('auth.force-password-reset');
Route::get('/subscription', [Controller::class, 'subscription'])->name('subscription'); Route::get('/subscription', [Controller::class, 'subscription'])->name('subscription');
Route::get('/settings', [Controller::class, 'settings'])->name('settings.configuration'); Route::get('/settings', [Controller::class, 'settings'])->name('settings.configuration');
Route::get('/settings/license', [Controller::class, 'license'])->name('settings.license'); Route::get('/settings/license', [Controller::class, 'license'])->name('settings.license');

View File

@ -6,6 +6,7 @@
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Subscription; use App\Models\Subscription;
use App\Models\Team; use App\Models\Team;
use App\Models\Waitlist;
use App\Models\Webhook; use App\Models\Webhook;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@ -139,7 +140,7 @@
ApplicationPreview::create([ ApplicationPreview::create([
'application_id' => $application->id, 'application_id' => $application->id,
'pull_request_id' => $pull_request_id, 'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url 'pull_request_html_url' => $pull_request_html_url,
]); ]);
} }
queue_application_deployment( queue_application_deployment(
@ -175,6 +176,38 @@
}); });
if (is_cloud()) { if (is_cloud()) {
Route::get('/waitlist/confirm', function () {
$email = request()->get('email');
$confirmation_code = request()->get('confirmation_code');
ray($email, $confirmation_code);
try {
$found = Waitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
if ($found && !$found->verified && $found->created_at > now()->subMinutes(config('constants.waitlist.confirmation_valid_for_minutes'))) {
$found->verified = true;
$found->save();
return 'Thank you for confirming your email address. We will notify you when you are next in line.';
}
return redirect()->route('dashboard');
} catch (error) {
return redirect()->route('dashboard');
}
})->name('webhooks.waitlist.confirm');
Route::get('/waitlist/cancel', function () {
$email = request()->get('email');
$confirmation_code = request()->get('confirmation_code');
try {
$found = Waitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
if ($found && !$found->verified) {
$found->delete();
return 'Your email address has been removed from the waitlist.';
}
return redirect()->route('dashboard');
} catch (error) {
return redirect()->route('dashboard');
}
})->name('webhooks.waitlist.cancel');
Route::post('/payments/events', function () { Route::post('/payments/events', function () {
try { try {
$secret = config('coolify.lemon_squeezy_webhook_secret'); $secret = config('coolify.lemon_squeezy_webhook_secret');
@ -188,7 +221,7 @@
$webhook = Webhook::create([ $webhook = Webhook::create([
'type' => 'lemonsqueezy', 'type' => 'lemonsqueezy',
'payload' => $payload 'payload' => $payload,
]); ]);
$event = data_get($payload, 'meta.event_name'); $event = data_get($payload, 'meta.event_name');
ray('Subscription event: ' . $event); ray('Subscription event: ' . $event);
@ -256,7 +289,7 @@
ray($e->getMessage()); ray($e->getMessage());
$webhook->update([ $webhook->update([
'status' => 'failed', 'status' => 'failed',
'failure_reason' => $e->getMessage() 'failure_reason' => $e->getMessage(),
]); ]);
} finally { } finally {
return response('OK'); return response('OK');