diff --git a/.env.development.example b/.env.development.example index 43d3a10fd..c956daafd 100644 --- a/.env.development.example +++ b/.env.development.example @@ -6,6 +6,7 @@ USERID= GROUPID= ############################################################################################################ +APP_NAME=Coolify-localhost APP_ID=development APP_ENV=local APP_KEY= diff --git a/app/Console/Commands/WaitlistInvite.php b/app/Console/Commands/WaitlistInvite.php index 7374b1f2f..a3b47089a 100644 --- a/app/Console/Commands/WaitlistInvite.php +++ b/app/Console/Commands/WaitlistInvite.php @@ -6,20 +6,20 @@ use App\Models\Waitlist; use Illuminate\Console\Command; use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; class WaitlistInvite extends Command { - public Waitlist|null $next_patient = null; - public User|null $new_user = null; + public Waitlist|User|null $next_patient = null; public string|null $password = null; /** * The name and signature of the console command. * * @var string */ - protected $signature = 'waitlist:invite {email?}'; + protected $signature = 'waitlist:invite {email?} {--only-email}'; /** * The console command description. @@ -34,7 +34,16 @@ class WaitlistInvite extends Command public function handle() { if ($this->argument('email')) { - $this->next_patient = Waitlist::where('email', $this->argument('email'))->first(); + if ($this->option('only-email')) { + $this->next_patient = User::whereEmail($this->argument('email'))->first(); + $this->password = Str::password(); + $this->next_patient->update([ + 'password' => Hash::make($this->password), + 'force_password_reset' => true, + ]); + } else { + $this->next_patient = Waitlist::where('email', $this->argument('email'))->first(); + } if (!$this->next_patient) { $this->error("{$this->argument('email')} not found in the waitlist."); return; @@ -43,6 +52,10 @@ public function handle() $this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first(); } if ($this->next_patient) { + if ($this->option('only-email')) { + $this->send_email(); + return; + } $this->register_user(); $this->remove_from_waitlist(); $this->send_email(); @@ -55,7 +68,7 @@ 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([ + User::create([ 'name' => Str::of($this->next_patient->email)->before('@'), 'email' => $this->next_patient->email, 'password' => Hash::make($this->password), @@ -73,10 +86,14 @@ private function remove_from_waitlist() } private function send_email() { + ray($this->next_patient->email, $this->password); + $token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password"); + $loginLink = route('auth.link', ['token' => $token]); $mail = new MailMessage(); $mail->view('emails.waitlist-invitation', [ 'email' => $this->next_patient->email, 'password' => $this->password, + 'loginLink' => $loginLink, ]); $mail->subject('Congratulations! You are invited to join Coolify Cloud.'); send_user_an_email($mail, $this->next_patient->email); diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 39b0881e3..29d705faf 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -8,15 +8,41 @@ use App\Models\StandalonePostgresql; use App\Models\TeamInvitation; use App\Models\User; +use Auth; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Routing\Controller as BaseController; +use Illuminate\Support\Facades\Crypt; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Http; use Throwable; +use Str; + class Controller extends BaseController { use AuthorizesRequests, ValidatesRequests; + public function link() + { + $token = request()->get('token'); + if ($token) { + $decrypted = Crypt::decryptString($token); + $email = Str::of($decrypted)->before('@@@'); + $password = Str::of($decrypted)->after('@@@'); + $user = User::whereEmail($email)->first(); + if (!$user) { + return redirect()->route('login'); + } + if (Hash::check($password, $user->password)) { + Auth::login($user); + $team = $user->teams()->first(); + session(['currentTeam' => $team]); + return redirect()->route('dashboard'); + } + } + return redirect()->route('login')->with('error', 'Invalid credentials.'); + } public function subscription() { if (!isCloud()) { @@ -37,10 +63,12 @@ public function license() ]); } - public function force_passoword_reset() { + public function force_passoword_reset() + { return view('auth.force-password-reset'); } - public function boarding() { + public function boarding() + { if (currentTeam()->boarding || isDev()) { return view('boarding'); } else { diff --git a/app/Http/Livewire/ForcePasswordReset.php b/app/Http/Livewire/ForcePasswordReset.php index 96a273e80..df2b37691 100644 --- a/app/Http/Livewire/ForcePasswordReset.php +++ b/app/Http/Livewire/ForcePasswordReset.php @@ -18,22 +18,26 @@ class ForcePasswordReset extends Component 'password' => 'required|min:8', 'password_confirmation' => 'required|same:password', ]; - public function mount() { + public function mount() + { $this->email = auth()->user()->email; } - public function submit() { + public function submit() + { try { $this->rateLimit(10); $this->validate(); + $firstLogin = auth()->user()->created_at == auth()->user()->updated_at; 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); + if ($firstLogin) { + send_internal_notification('First login for ' . auth()->user()->email); + } + return redirect()->route('dashboard'); + } 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 index 8d1670de5..e8129deda 100644 --- a/app/Http/Middleware/CheckForcePasswordReset.php +++ b/app/Http/Middleware/CheckForcePasswordReset.php @@ -16,6 +16,12 @@ class CheckForcePasswordReset public function handle(Request $request, Closure $next): Response { if (auth()->user()) { + if ($request->path() === 'auth/link') { + auth()->logout(); + request()->session()->invalidate(); + request()->session()->regenerateToken(); + return $next($request); + } $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') { diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php index ee13a6cc2..3fee2acab 100644 --- a/app/Notifications/Internal/GeneralNotification.php +++ b/app/Notifications/Internal/GeneralNotification.php @@ -12,12 +12,12 @@ class GeneralNotification extends Notification implements ShouldQueue use Queueable; public function __construct(public string $message) - {} + { + } public function via(object $notifiable): array { - $channels[] = DiscordChannel::class; - return $channels; + return [DiscordChannel::class]; } public function toDiscord(): string diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php index 6844aa705..2a11051b3 100644 --- a/app/Notifications/TransactionalEmails/ResetPassword.php +++ b/app/Notifications/TransactionalEmails/ResetPassword.php @@ -50,10 +50,6 @@ public function toMail($notifiable) protected function buildMailMessage($url) { $mail = new MailMessage(); - $mail->from( - data_get($this->settings, 'smtp_from_address'), - data_get($this->settings, 'smtp_from_name'), - ); $mail->subject('Reset Password'); $mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]); return $mail; diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index a5fce4858..66d85bf13 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -57,6 +57,7 @@ public function boot(): void }); Fortify::loginView(function () { + abort(503,'Login is disabled'); $settings = InstanceSettings::get(); $users = User::count(); if ($users == 0) { diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index e45dfa08a..1d3dba13d 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -242,7 +242,8 @@ function validate_cron_expression($expression_to_validate): bool function send_internal_notification(string $message): void { try { - $baseUrl = base_url(false); + ray('Sending internal notification... 📬 ' . $message); + $baseUrl = config('app.name'); $team = Team::find(0); $team->notify(new GeneralNotification("👀 Internal notifications from {$baseUrl}: " . $message)); } catch (\Throwable $th) { diff --git a/database/migrations/2023_08_22_071054_add_stripe_reasons.php b/database/migrations/2023_08_22_071054_add_stripe_reasons.php new file mode 100644 index 000000000..98f85c921 --- /dev/null +++ b/database/migrations/2023_08_22_071054_add_stripe_reasons.php @@ -0,0 +1,32 @@ +string('stripe_feedback')->nullable()->after('stripe_cancel_at_period_end'); + $table->string('stripe_comment')->nullable()->after('stripe_feedback'); + + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('subscriptions', function (Blueprint $table) { + $table->dropColumn('stripe_feedback'); + $table->dropColumn('stripe_comment'); + }); + } +}; diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 838bc5a11..2ed0564bb 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -51,6 +51,11 @@ class="text-xs normal-case hover:no-underline btn btn-sm bg-coollabs-gradient"> {{ session('status') }} @endif + @if (session('error')) +
+ {{ session('error') }} +
+ @endif diff --git a/resources/views/components/emails/footer.blade.php b/resources/views/components/emails/footer.blade.php index 31077fabf..44f47ef93 100644 --- a/resources/views/components/emails/footer.blade.php +++ b/resources/views/components/emails/footer.blade.php @@ -1,6 +1,6 @@ {{ Illuminate\Mail\Markdown::parse('---') }} -Thank you.
+Thank you,
{{ config('app.name') ?? 'Coolify' }} {{ Illuminate\Mail\Markdown::parse('[Contact Support](https://docs.coollabs.io)') }} diff --git a/resources/views/emails/reset-password.blade.php b/resources/views/emails/reset-password.blade.php index a50aa9eca..02488175d 100644 --- a/resources/views/emails/reset-password.blade.php +++ b/resources/views/emails/reset-password.blade.php @@ -1,5 +1,5 @@ -A password reset has been requested for this email address on [{{ config('app.name') }}]({{ config('app.url') }}). +A password reset has been requested for this email address. Click [here]({{ $url }}) to reset your password. diff --git a/resources/views/emails/waitlist-invitation.blade.php b/resources/views/emails/waitlist-invitation.blade.php index 89ee8fa52..bae5e329a 100644 --- a/resources/views/emails/waitlist-invitation.blade.php +++ b/resources/views/emails/waitlist-invitation.blade.php @@ -1,19 +1,4 @@ -You have been invited to join the Coolify Cloud. - -[Login here]({{base_url()}}/login) - -Here is your initial login information. - -Email: - -**{{ $email }}** - -Initial Password: - -**{{ $password }}** - -(You will forced to change it on first login.) - +You have been invited to join the Coolify Cloud: [Get Started]({{$loginLink}}) diff --git a/resources/views/errors/401.blade.php b/resources/views/errors/401.blade.php index 5c586db96..ebf5a1035 100644 --- a/resources/views/errors/401.blade.php +++ b/resources/views/errors/401.blade.php @@ -1,5 +1,20 @@ -@extends('errors::minimal') - -@section('title', __('Unauthorized')) -@section('code', '401') -@section('message', __('Unauthorized')) +@extends('layouts.base') +
+
+
+

401

+

You shall not pass!

+

You don't have permission to access this page. +

+ +
+
+
diff --git a/resources/views/errors/403.blade.php b/resources/views/errors/403.blade.php index a5506f01f..dc8be5fd4 100644 --- a/resources/views/errors/403.blade.php +++ b/resources/views/errors/403.blade.php @@ -1,5 +1,20 @@ -@extends('errors::minimal') - -@section('title', __('Forbidden')) -@section('code', '403') -@section('message', __($exception->getMessage() ?: 'Forbidden')) +@extends('layouts.base') +
+
+
+

403

+

You shall not pass!

+

You don't have permission to access this page. +

+ +
+
+
diff --git a/resources/views/errors/404.blade.php b/resources/views/errors/404.blade.php index 3215b129f..d19737391 100644 --- a/resources/views/errors/404.blade.php +++ b/resources/views/errors/404.blade.php @@ -1,22 +1,21 @@ - -
-
-
-

404

-

How did you got here?

-

Sorry, we couldn’t find the page you’re looking - for. -

- +@extends('layouts.base') +
+
+
+

404

+

How did you got here?

+

Sorry, we couldn’t find the page you’re looking + for. +

+
- +
diff --git a/resources/views/errors/419.blade.php b/resources/views/errors/419.blade.php index 993009074..05d296426 100644 --- a/resources/views/errors/419.blade.php +++ b/resources/views/errors/419.blade.php @@ -1,21 +1,20 @@ - -
-
-
-

419

-

This page is definitely old

-

Sorry, we couldn’t find the page you’re looking - for. -

- +@extends('layouts.base') +
+
+
+

419

+

This page is definitely old, not like you!

+

Sorry, we couldn’t find the page you’re looking + for. +

+
- +
diff --git a/resources/views/errors/429.blade.php b/resources/views/errors/429.blade.php index f01b07b8e..608591095 100644 --- a/resources/views/errors/429.blade.php +++ b/resources/views/errors/429.blade.php @@ -1,5 +1,19 @@ -@extends('errors::minimal') - -@section('title', __('Too Many Requests')) -@section('code', '429') -@section('message', __('Too Many Requests')) +@extends('layouts.base') +
+
+
+

429

+

Woah, slow down there!

+

You're making too many requests. Please wait a few seconds before trying again. +

+ +
+
+
diff --git a/resources/views/errors/500.blade.php b/resources/views/errors/500.blade.php index d9e95d9b9..65c8de72d 100644 --- a/resources/views/errors/500.blade.php +++ b/resources/views/errors/500.blade.php @@ -1,5 +1,23 @@ -@extends('errors::minimal') - -@section('title', __('Server Error')) -@section('code', '500') -@section('message', __('Server Error')) +@extends('layouts.base') +
+
+
+

500

+

Something is not okay, are you okay?

+

There has been an error, we are working on it. +

+ @if ($exception->getMessage() !== '') +

Error: {{ $exception->getMessage() }} +

+ @endif + +
+
+
diff --git a/resources/views/errors/503.blade.php b/resources/views/errors/503.blade.php index c5a9dde14..445dc24de 100644 --- a/resources/views/errors/503.blade.php +++ b/resources/views/errors/503.blade.php @@ -1,5 +1,17 @@ -@extends('errors::minimal') - -@section('title', __('Service Unavailable')) -@section('code', '503') -@section('message', __('Service Unavailable')) +@extends('layouts.base') +
+
+
+

503

+

We are working on serious things.

+

Service Unavailable. Be right back. Thanks for your + patience. +

+ +
+
+
diff --git a/resources/views/errors/layout.blade.php b/resources/views/errors/layout.blade.php deleted file mode 100644 index a08d57aeb..000000000 --- a/resources/views/errors/layout.blade.php +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - @yield('title') - - - - - - -
-
-
- @yield('message') -
-
-
- - - diff --git a/resources/views/errors/minimal.blade.php b/resources/views/errors/minimal.blade.php deleted file mode 100644 index f54e53d10..000000000 --- a/resources/views/errors/minimal.blade.php +++ /dev/null @@ -1,552 +0,0 @@ - - - - - - - - @yield('title') - - - - - - - -
-
-
-
- @yield('code') -
- -
- @yield('message') -
-
-
-
- - - diff --git a/routes/web.php b/routes/web.php index 6d96710af..6094a4a43 100644 --- a/routes/web.php +++ b/routes/web.php @@ -49,7 +49,9 @@ return response()->json(['message' => 'Transactional emails are not active'], 400); })->name('password.forgot'); Route::get('/waitlist', WaitlistIndex::class)->name('waitlist.index'); - +Route::middleware(['throttle:login'])->group(function() { + Route::get('/auth/link', [Controller::class, 'link'])->name('auth.link'); +}); Route::prefix('magic')->middleware(['auth'])->group(function () { Route::get('/servers', [MagicController::class, 'servers']); Route::get('/destinations', [MagicController::class, 'destinations']); diff --git a/routes/webhooks.php b/routes/webhooks.php index a724e8e70..51ab33166 100644 --- a/routes/webhooks.php +++ b/routes/webhooks.php @@ -225,6 +225,7 @@ ]); $type = data_get($event, 'type'); $data = data_get($event, 'data.object'); + ray('Event: '. $type); switch ($type) { case 'checkout.session.completed': $clientReferenceId = data_get($data, 'client_reference_id'); @@ -239,12 +240,14 @@ } $subscription = Subscription::where('team_id', $teamId)->first(); if ($subscription) { + send_internal_notification('Old subscription activated for team: ' . $teamId); $subscription->update([ 'stripe_subscription_id' => $subscriptionId, 'stripe_customer_id' => $customerId, 'stripe_invoice_paid' => true, ]); } else { + send_internal_notification('New subscription for team: ' . $teamId); Subscription::create([ 'team_id' => $teamId, 'stripe_subscription_id' => $subscriptionId, @@ -254,46 +257,67 @@ } break; case 'invoice.paid': - $subscriptionId = data_get($data, 'lines.data.0.subscription'); - $subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail(); + $customerId = data_get($data, 'customer'); + $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $subscription->update([ 'stripe_invoice_paid' => true, ]); break; - case 'invoice.payment_failed': - $customerId = data_get($data, 'customer'); - $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if (!$subscription) { - return; - } - SubscriptionInvoiceFailedJob::dispatch($subscription->team); - break; + // case 'invoice.payment_failed': + // $customerId = data_get($data, 'customer'); + // $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + // if ($subscription) { + // SubscriptionInvoiceFailedJob::dispatch($subscription->team); + // } + // break; case 'customer.subscription.updated': + $customerId = data_get($data, 'customer'); + $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $subscriptionId = data_get($data, 'items.data.0.subscription'); $planId = data_get($data, 'items.data.0.plan.id'); $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end'); - $subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail(); + $alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_cancel_at_period_end'); + $feedback = data_get($data, 'cancellation_details.feedback'); + $comment = data_get($data, 'cancellation_details.comment'); $subscription->update([ + 'stripe_feedback' => $feedback, + 'stripe_comment' => $comment, 'stripe_plan_id' => $planId, 'stripe_cancel_at_period_end' => $cancelAtPeriodEnd, ]); + ray($feedback, $comment, $alreadyCancelAtPeriodEnd, $cancelAtPeriodEnd); + if ($feedback) { + $reason = "Cancellation feedback for {$subscription->team->id}: '" . $feedback ."'"; + if ($comment) { + $reason .= ' with comment: \'' . $comment ."'"; + } + send_internal_notification($reason); + } + ray($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd); + if ($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd) { + if ($cancelAtPeriodEnd) { + send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id); + } else { + send_internal_notification('Subscription resumed for team: ' . $subscription->team->id); + } + } break; case 'customer.subscription.deleted': - $subscriptionId = data_get($data, 'items.data.0.subscription'); - $subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail(); + $customerId = data_get($data, 'customer'); + $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $subscription->update([ 'stripe_subscription_id' => null, 'stripe_plan_id'=> null, 'stripe_cancel_at_period_end' => false, 'stripe_invoice_paid' => false, ]); + send_internal_notification('Subscription cancelled: ' . $subscription->team->id); break; default: // Unhandled event type } } catch (Exception $e) { - ray($e->getMessage()); - send_internal_notification('Subscription webhook failed: ' . $e->getMessage()); + send_internal_notification("Subscription webhook ($type) failed: " . $e->getMessage()); $webhook->update([ 'status' => 'failed', 'failure_reason' => $e->getMessage(),