fix: stripe

add: custom error pages
fix: invititation
feat: new quick login for first users (UX++)
feat: more internal notifications
This commit is contained in:
Andras Bacsai 2023-09-06 12:07:34 +02:00
parent 0dbb8b4420
commit e7c0c26b32
25 changed files with 292 additions and 727 deletions

View File

@ -6,6 +6,7 @@
USERID= USERID=
GROUPID= GROUPID=
############################################################################################################ ############################################################################################################
APP_NAME=Coolify-localhost
APP_ID=development APP_ID=development
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=

View File

@ -6,20 +6,20 @@
use App\Models\Waitlist; use App\Models\Waitlist;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class WaitlistInvite extends Command class WaitlistInvite extends Command
{ {
public Waitlist|null $next_patient = null; public Waitlist|User|null $next_patient = null;
public User|null $new_user = null;
public string|null $password = null; public string|null $password = null;
/** /**
* The name and signature of the console command. * The name and signature of the console command.
* *
* @var string * @var string
*/ */
protected $signature = 'waitlist:invite {email?}'; protected $signature = 'waitlist:invite {email?} {--only-email}';
/** /**
* The console command description. * The console command description.
@ -34,7 +34,16 @@ class WaitlistInvite extends Command
public function handle() public function handle()
{ {
if ($this->argument('email')) { if ($this->argument('email')) {
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(); $this->next_patient = Waitlist::where('email', $this->argument('email'))->first();
}
if (!$this->next_patient) { if (!$this->next_patient) {
$this->error("{$this->argument('email')} not found in the waitlist."); $this->error("{$this->argument('email')} not found in the waitlist.");
return; return;
@ -43,6 +52,10 @@ public function handle()
$this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first(); $this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first();
} }
if ($this->next_patient) { if ($this->next_patient) {
if ($this->option('only-email')) {
$this->send_email();
return;
}
$this->register_user(); $this->register_user();
$this->remove_from_waitlist(); $this->remove_from_waitlist();
$this->send_email(); $this->send_email();
@ -55,7 +68,7 @@ private function register_user()
$already_registered = User::whereEmail($this->next_patient->email)->first(); $already_registered = User::whereEmail($this->next_patient->email)->first();
if (!$already_registered) { if (!$already_registered) {
$this->password = Str::password(); $this->password = Str::password();
$this->new_user = User::create([ User::create([
'name' => Str::of($this->next_patient->email)->before('@'), 'name' => Str::of($this->next_patient->email)->before('@'),
'email' => $this->next_patient->email, 'email' => $this->next_patient->email,
'password' => Hash::make($this->password), 'password' => Hash::make($this->password),
@ -73,10 +86,14 @@ private function remove_from_waitlist()
} }
private function send_email() 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 = new MailMessage();
$mail->view('emails.waitlist-invitation', [ $mail->view('emails.waitlist-invitation', [
'email' => $this->next_patient->email, 'email' => $this->next_patient->email,
'password' => $this->password, 'password' => $this->password,
'loginLink' => $loginLink,
]); ]);
$mail->subject('Congratulations! You are invited to join Coolify Cloud.'); $mail->subject('Congratulations! You are invited to join Coolify Cloud.');
send_user_an_email($mail, $this->next_patient->email); send_user_an_email($mail, $this->next_patient->email);

View File

@ -8,15 +8,41 @@
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 Auth;
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;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
use Throwable; use Throwable;
use Str;
class Controller extends BaseController class Controller extends BaseController
{ {
use AuthorizesRequests, ValidatesRequests; 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() public function subscription()
{ {
if (!isCloud()) { 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'); return view('auth.force-password-reset');
} }
public function boarding() { public function boarding()
{
if (currentTeam()->boarding || isDev()) { if (currentTeam()->boarding || isDev()) {
return view('boarding'); return view('boarding');
} else { } else {

View File

@ -18,22 +18,26 @@ class ForcePasswordReset extends Component
'password' => 'required|min:8', 'password' => 'required|min:8',
'password_confirmation' => 'required|same:password', 'password_confirmation' => 'required|same:password',
]; ];
public function mount() { public function mount()
{
$this->email = auth()->user()->email; $this->email = auth()->user()->email;
} }
public function submit() { public function submit()
{
try { try {
$this->rateLimit(10); $this->rateLimit(10);
$this->validate(); $this->validate();
$firstLogin = auth()->user()->created_at == auth()->user()->updated_at;
auth()->user()->forceFill([ auth()->user()->forceFill([
'password' => Hash::make($this->password), 'password' => Hash::make($this->password),
'force_password_reset' => false, 'force_password_reset' => false,
])->save(); ])->save();
auth()->logout(); if ($firstLogin) {
return redirect()->route('login')->with('status', 'Your initial password has been set.'); send_internal_notification('First login for ' . auth()->user()->email);
} catch(\Exception $e) { }
return general_error_handler(err:$e, that:$this); return redirect()->route('dashboard');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} }
} }
} }

View File

@ -16,6 +16,12 @@ class CheckForcePasswordReset
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
if (auth()->user()) { 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; $force_password_reset = auth()->user()->force_password_reset;
if ($force_password_reset) { if ($force_password_reset) {
if ($request->routeIs('auth.force-password-reset') || $request->path() === 'livewire/message/force-password-reset') { if ($request->routeIs('auth.force-password-reset') || $request->path() === 'livewire/message/force-password-reset') {

View File

@ -12,12 +12,12 @@ class GeneralNotification extends Notification implements ShouldQueue
use Queueable; use Queueable;
public function __construct(public string $message) public function __construct(public string $message)
{} {
}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
$channels[] = DiscordChannel::class; return [DiscordChannel::class];
return $channels;
} }
public function toDiscord(): string public function toDiscord(): string

View File

@ -50,10 +50,6 @@ public function toMail($notifiable)
protected function buildMailMessage($url) protected function buildMailMessage($url)
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->from(
data_get($this->settings, 'smtp_from_address'),
data_get($this->settings, 'smtp_from_name'),
);
$mail->subject('Reset Password'); $mail->subject('Reset Password');
$mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]); $mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]);
return $mail; return $mail;

View File

@ -57,6 +57,7 @@ public function boot(): void
}); });
Fortify::loginView(function () { Fortify::loginView(function () {
abort(503,'Login is disabled');
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
$users = User::count(); $users = User::count();
if ($users == 0) { if ($users == 0) {

View File

@ -242,7 +242,8 @@ function validate_cron_expression($expression_to_validate): bool
function send_internal_notification(string $message): void function send_internal_notification(string $message): void
{ {
try { try {
$baseUrl = base_url(false); ray('Sending internal notification... 📬 ' . $message);
$baseUrl = config('app.name');
$team = Team::find(0); $team = Team::find(0);
$team->notify(new GeneralNotification("👀 Internal notifications from {$baseUrl}: " . $message)); $team->notify(new GeneralNotification("👀 Internal notifications from {$baseUrl}: " . $message));
} catch (\Throwable $th) { } catch (\Throwable $th) {

View File

@ -0,0 +1,32 @@
<?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('subscriptions', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -51,6 +51,11 @@ class="text-xs normal-case hover:no-underline btn btn-sm bg-coollabs-gradient">
{{ session('status') }} {{ session('status') }}
</div> </div>
@endif @endif
@if (session('error'))
<div class="mb-4 font-medium text-red-600">
{{ session('error') }}
</div>
@endif
</form> </form>
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
{{ Illuminate\Mail\Markdown::parse('---') }} {{ Illuminate\Mail\Markdown::parse('---') }}
Thank you.<br> Thank you,<br>
{{ config('app.name') ?? 'Coolify' }} {{ config('app.name') ?? 'Coolify' }}
{{ Illuminate\Mail\Markdown::parse('[Contact Support](https://docs.coollabs.io)') }} {{ Illuminate\Mail\Markdown::parse('[Contact Support](https://docs.coollabs.io)') }}

View File

@ -1,5 +1,5 @@
<x-emails.layout> <x-emails.layout>
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. Click [here]({{ $url }}) to reset your password.

View File

@ -1,19 +1,4 @@
<x-emails.layout> <x-emails.layout>
You have been invited to join the Coolify Cloud. You have been invited to join the Coolify Cloud: [Get Started]({{$loginLink}})
[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.)
</x-emails.layout> </x-emails.layout>

View File

@ -1,5 +1,20 @@
@extends('errors::minimal') @extends('layouts.base')
<div class="min-h-screen hero">
@section('title', __('Unauthorized')) <div class="text-center hero-content">
@section('code', '401') <div class="">
@section('message', __('Unauthorized')) <p class="font-mono text-6xl font-semibold text-warning">401</p>
<h1 class="mt-4 font-bold tracking-tight text-white">You shall not pass!</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">You don't have permission to access this page.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a target="_blank" class="text-xs" href="https://docs.coollabs.io/contact.html">Contact
support
<x-external-link />
</a>
</div>
</div>
</div>
</div>

View File

@ -1,5 +1,20 @@
@extends('errors::minimal') @extends('layouts.base')
<div class="min-h-screen hero">
@section('title', __('Forbidden')) <div class="text-center hero-content">
@section('code', '403') <div class="">
@section('message', __($exception->getMessage() ?: 'Forbidden')) <p class="font-mono text-6xl font-semibold text-warning">403</p>
<h1 class="mt-4 font-bold tracking-tight text-white">You shall not pass!</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">You don't have permission to access this page.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a target="_blank" class="text-xs" href="https://docs.coollabs.io/contact.html">Contact
support
<x-external-link />
</a>
</div>
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
<x-layout> @extends('layouts.base')
<div class="min-h-screen hero"> <div class="min-h-screen hero">
<div class="text-center hero-content"> <div class="text-center hero-content">
<div class=""> <div class="">
<p class="font-mono text-6xl font-semibold text-warning">404</p> <p class="font-mono text-6xl font-semibold text-warning">404</p>
@ -9,7 +9,7 @@
</p> </p>
<div class="flex items-center justify-center mt-10 gap-x-6"> <div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/"> <a href="/">
<x-forms.button isHighlighted>Go back home</x-forms.button> <x-forms.button>Go back home</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="https://docs.coollabs.io/contact.html">Contact <a target="_blank" class="text-xs" href="https://docs.coollabs.io/contact.html">Contact
support support
@ -18,5 +18,4 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</x-layout>

View File

@ -1,21 +1,20 @@
<x-layout> @extends('layouts.base')
<div class="min-h-screen hero"> <div class="min-h-screen hero">
<div class="text-center hero-content"> <div class="text-center hero-content">
<div class=""> <div class="">
<p class="font-mono text-6xl font-semibold text-warning">419</p> <p class="font-mono text-6xl font-semibold text-warning">419</p>
<h1 class="mt-4 font-bold tracking-tight text-white">This page is definitely old</h1> <h1 class="mt-4 font-bold tracking-tight text-white">This page is definitely old, not like you!</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">Sorry, we couldnt find the page youre looking <p class="mt-6 text-base leading-7 text-neutral-300">Sorry, we couldnt find the page youre looking
for. for.
</p> </p>
<div class="flex items-center justify-center mt-10 gap-x-6"> <div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/" <a href="/">
class="rounded-md bg-coollabs px-3.5 py-2.5 font-semibold text-white shadow-sm hover:bg-coollabs-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 hover:no-underline">Go <x-forms.button>Go back home</x-forms.button>
back home</a> </a>
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact <a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact
support support
<span aria-hidden="true">&rarr;</span></a> <span aria-hidden="true">&rarr;</span></a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</x-layout>

View File

@ -1,5 +1,19 @@
@extends('errors::minimal') @extends('layouts.base')
<div class="min-h-screen hero">
@section('title', __('Too Many Requests')) <div class="text-center hero-content">
@section('code', '429') <div class="">
@section('message', __('Too Many Requests')) <p class="font-mono text-6xl font-semibold text-warning">429</p>
<h1 class="mt-4 font-bold tracking-tight text-white">Woah, slow down there!</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">You're making too many requests. Please wait a few seconds before trying again.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact
support
<span aria-hidden="true">&rarr;</span></a>
</div>
</div>
</div>
</div>

View File

@ -1,5 +1,23 @@
@extends('errors::minimal') @extends('layouts.base')
<div class="min-h-screen hero ">
@section('title', __('Server Error')) <div class="text-center hero-content">
@section('code', '500') <div>
@section('message', __('Server Error')) <p class="font-mono text-6xl font-semibold text-warning">500</p>
<h1 class="mt-4 font-bold tracking-tight text-white">Something is not okay, are you okay?</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">There has been an error, we are working on it.
</p>
@if ($exception->getMessage() !== '')
<p class="mt-6 text-base leading-7 text-red-500">Error: {{ $exception->getMessage() }}
</p>
@endif
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact
support
<span aria-hidden="true">&rarr;</span></a>
</div>
</div>
</div>
</div>

View File

@ -1,5 +1,17 @@
@extends('errors::minimal') @extends('layouts.base')
<div class="min-h-screen hero ">
@section('title', __('Service Unavailable')) <div class="text-center hero-content">
@section('code', '503') <div>
@section('message', __('Service Unavailable')) <p class="font-mono text-6xl font-semibold text-warning">503</p>
<h1 class="mt-4 font-bold tracking-tight text-white">We are working on serious things.</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">Service Unavailable. Be right back. Thanks for your
patience.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact
support
<span aria-hidden="true">&rarr;</span></a>
</div>
</div>
</div>
</div>

View File

@ -1,57 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title')</title>
<!-- Styles -->
<style>
html,
body {
background-color: #fff;
color: #636b6f;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-weight: 100;
height: 100vh;
margin: 0;
}
.full-height {
height: 100vh;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center;
}
.position-ref {
position: relative;
}
.content {
text-align: center;
}
.title {
font-size: 36px;
padding: 20px;
}
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
<div class="content">
<div class="title">
@yield('message')
</div>
</div>
</div>
</body>
</html>

View File

@ -1,552 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title')</title>
<style>
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
html {
line-height: 1.15;
-webkit-text-size-adjust: 100%
}
body {
margin: 0
}
a {
background-color: transparent
}
code {
font-family: monospace, monospace;
font-size: 1em
}
[hidden] {
display: none
}
html {
font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
line-height: 1.5
}
*,
:after,
:before {
box-sizing: border-box;
border: 0 solid #e2e8f0
}
a {
color: inherit;
text-decoration: inherit
}
code {
font-family: Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace
}
svg,
video {
display: block;
vertical-align: middle
}
video {
max-width: 100%;
height: auto
}
.bg-white {
--bg-opacity: 1;
background-color: #fff;
background-color: rgba(255, 255, 255, var(--bg-opacity))
}
.bg-gray-100 {
--bg-opacity: 1;
background-color: #f7fafc;
background-color: rgba(247, 250, 252, var(--bg-opacity))
}
.border-gray-200 {
--border-opacity: 1;
border-color: #edf2f7;
border-color: rgba(237, 242, 247, var(--border-opacity))
}
.border-gray-400 {
--border-opacity: 1;
border-color: #cbd5e0;
border-color: rgba(203, 213, 224, var(--border-opacity))
}
.border-t {
border-top-width: 1px
}
.border-r {
border-right-width: 1px
}
.flex {
display: flex
}
.grid {
display: grid
}
.hidden {
display: none
}
.items-center {
align-items: center
}
.justify-center {
justify-content: center
}
.font-semibold {
font-weight: 600
}
.h-5 {
height: 1.25rem
}
.h-8 {
height: 2rem
}
.h-16 {
height: 4rem
}
.text-sm {
font-size: .875rem
}
.text-lg {
font-size: 1.125rem
}
.leading-7 {
line-height: 1.75rem
}
.mx-auto {
margin-left: auto;
margin-right: auto
}
.ml-1 {
margin-left: .25rem
}
.mt-2 {
margin-top: .5rem
}
.mr-2 {
margin-right: .5rem
}
.ml-2 {
margin-left: .5rem
}
.mt-4 {
margin-top: 1rem
}
.ml-4 {
margin-left: 1rem
}
.mt-8 {
margin-top: 2rem
}
.ml-12 {
margin-left: 3rem
}
.-mt-px {
margin-top: -1px
}
.max-w-xl {
max-width: 36rem
}
.max-w-6xl {
max-width: 72rem
}
.min-h-screen {
min-height: 100vh
}
.overflow-hidden {
overflow: hidden
}
.p-6 {
padding: 1.5rem
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem
}
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem
}
.pt-8 {
padding-top: 2rem
}
.fixed {
position: fixed
}
.relative {
position: relative
}
.top-0 {
top: 0
}
.right-0 {
right: 0
}
.shadow {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06)
}
.text-center {
text-align: center
}
.text-gray-200 {
--text-opacity: 1;
color: #edf2f7;
color: rgba(237, 242, 247, var(--text-opacity))
}
.text-gray-300 {
--text-opacity: 1;
color: #e2e8f0;
color: rgba(226, 232, 240, var(--text-opacity))
}
.text-gray-400 {
--text-opacity: 1;
color: #cbd5e0;
color: rgba(203, 213, 224, var(--text-opacity))
}
.text-gray-500 {
--text-opacity: 1;
color: #a0aec0;
color: rgba(160, 174, 192, var(--text-opacity))
}
.text-gray-600 {
--text-opacity: 1;
color: #718096;
color: rgba(113, 128, 150, var(--text-opacity))
}
.text-gray-700 {
--text-opacity: 1;
color: #4a5568;
color: rgba(74, 85, 104, var(--text-opacity))
}
.text-gray-900 {
--text-opacity: 1;
color: #1a202c;
color: rgba(26, 32, 44, var(--text-opacity))
}
.uppercase {
text-transform: uppercase
}
.underline {
text-decoration: underline
}
.antialiased {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale
}
.tracking-wider {
letter-spacing: .05em
}
.w-5 {
width: 1.25rem
}
.w-8 {
width: 2rem
}
.w-auto {
width: auto
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr))
}
@-webkit-keyframes spin {
0% {
transform: rotate(0deg)
}
to {
transform: rotate(1turn)
}
}
@keyframes spin {
0% {
transform: rotate(0deg)
}
to {
transform: rotate(1turn)
}
}
@-webkit-keyframes ping {
0% {
transform: scale(1);
opacity: 1
}
75%,
to {
transform: scale(2);
opacity: 0
}
}
@keyframes ping {
0% {
transform: scale(1);
opacity: 1
}
75%,
to {
transform: scale(2);
opacity: 0
}
}
@-webkit-keyframes pulse {
0%,
to {
opacity: 1
}
50% {
opacity: .5
}
}
@keyframes pulse {
0%,
to {
opacity: 1
}
50% {
opacity: .5
}
}
@-webkit-keyframes bounce {
0%,
to {
transform: translateY(-25%);
-webkit-animation-timing-function: cubic-bezier(.8, 0, 1, 1);
animation-timing-function: cubic-bezier(.8, 0, 1, 1)
}
50% {
transform: translateY(0);
-webkit-animation-timing-function: cubic-bezier(0, 0, .2, 1);
animation-timing-function: cubic-bezier(0, 0, .2, 1)
}
}
@keyframes bounce {
0%,
to {
transform: translateY(-25%);
-webkit-animation-timing-function: cubic-bezier(.8, 0, 1, 1);
animation-timing-function: cubic-bezier(.8, 0, 1, 1)
}
50% {
transform: translateY(0);
-webkit-animation-timing-function: cubic-bezier(0, 0, .2, 1);
animation-timing-function: cubic-bezier(0, 0, .2, 1)
}
}
@media (min-width: 640px) {
.sm\:rounded-lg {
border-radius: .5rem
}
.sm\:block {
display: block
}
.sm\:items-center {
align-items: center
}
.sm\:justify-start {
justify-content: flex-start
}
.sm\:justify-between {
justify-content: space-between
}
.sm\:h-20 {
height: 5rem
}
.sm\:ml-0 {
margin-left: 0
}
.sm\:px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem
}
.sm\:pt-0 {
padding-top: 0
}
.sm\:text-left {
text-align: left
}
.sm\:text-right {
text-align: right
}
}
@media (min-width: 768px) {
.md\:border-t-0 {
border-top-width: 0
}
.md\:border-l {
border-left-width: 1px
}
.md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr))
}
}
@media (min-width: 1024px) {
.lg\:px-8 {
padding-left: 2rem;
padding-right: 2rem
}
}
@media (prefers-color-scheme: dark) {
.dark\:bg-gray-800 {
--bg-opacity: 1;
background-color: #2d3748;
background-color: rgba(45, 55, 72, var(--bg-opacity))
}
.dark\:bg-gray-900 {
--bg-opacity: 1;
background-color: #1a202c;
background-color: rgba(26, 32, 44, var(--bg-opacity))
}
.dark\:border-gray-700 {
--border-opacity: 1;
border-color: #4a5568;
border-color: rgba(74, 85, 104, var(--border-opacity))
}
.dark\:text-white {
--text-opacity: 1;
color: #fff;
color: rgba(255, 255, 255, var(--text-opacity))
}
.dark\:text-gray-400 {
--text-opacity: 1;
color: #cbd5e0;
color: rgba(203, 213, 224, var(--text-opacity))
}
}
</style>
<style>
body {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
</style>
</head>
<body class="antialiased">
<div
class="relative flex justify-center min-h-screen bg-gray-100 items-top dark:bg-gray-900 sm:items-center sm:pt-0">
<div class="max-w-xl mx-auto sm:px-6 lg:px-8">
<div class="flex items-center pt-8 sm:justify-start sm:pt-0">
<div class="px-4 text-lg tracking-wider text-gray-500 border-r border-gray-400">
@yield('code')
</div>
<div class="ml-4 text-lg tracking-wider text-gray-500 uppercase">
@yield('message')
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -49,7 +49,9 @@
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', WaitlistIndex::class)->name('waitlist.index'); 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::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']);

View File

@ -225,6 +225,7 @@
]); ]);
$type = data_get($event, 'type'); $type = data_get($event, 'type');
$data = data_get($event, 'data.object'); $data = data_get($event, 'data.object');
ray('Event: '. $type);
switch ($type) { switch ($type) {
case 'checkout.session.completed': case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id'); $clientReferenceId = data_get($data, 'client_reference_id');
@ -239,12 +240,14 @@
} }
$subscription = Subscription::where('team_id', $teamId)->first(); $subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) { if ($subscription) {
send_internal_notification('Old subscription activated for team: ' . $teamId);
$subscription->update([ $subscription->update([
'stripe_subscription_id' => $subscriptionId, 'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId, 'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true, 'stripe_invoice_paid' => true,
]); ]);
} else { } else {
send_internal_notification('New subscription for team: ' . $teamId);
Subscription::create([ Subscription::create([
'team_id' => $teamId, 'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId, 'stripe_subscription_id' => $subscriptionId,
@ -254,46 +257,67 @@
} }
break; break;
case 'invoice.paid': case 'invoice.paid':
$subscriptionId = data_get($data, 'lines.data.0.subscription'); $customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail(); $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$subscription->update([ $subscription->update([
'stripe_invoice_paid' => true, 'stripe_invoice_paid' => true,
]); ]);
break; break;
case 'invoice.payment_failed': // case 'invoice.payment_failed':
$customerId = data_get($data, 'customer'); // $customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first(); // $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (!$subscription) { // if ($subscription) {
return; // SubscriptionInvoiceFailedJob::dispatch($subscription->team);
} // }
SubscriptionInvoiceFailedJob::dispatch($subscription->team); // break;
break;
case 'customer.subscription.updated': 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'); $subscriptionId = data_get($data, 'items.data.0.subscription');
$planId = data_get($data, 'items.data.0.plan.id'); $planId = data_get($data, 'items.data.0.plan.id');
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end'); $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([ $subscription->update([
'stripe_feedback' => $feedback,
'stripe_comment' => $comment,
'stripe_plan_id' => $planId, 'stripe_plan_id' => $planId,
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd, '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; break;
case 'customer.subscription.deleted': case 'customer.subscription.deleted':
$subscriptionId = data_get($data, 'items.data.0.subscription'); $customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail(); $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$subscription->update([ $subscription->update([
'stripe_subscription_id' => null, 'stripe_subscription_id' => null,
'stripe_plan_id'=> null, 'stripe_plan_id'=> null,
'stripe_cancel_at_period_end' => false, 'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false, 'stripe_invoice_paid' => false,
]); ]);
send_internal_notification('Subscription cancelled: ' . $subscription->team->id);
break; break;
default: default:
// Unhandled event type // Unhandled event type
} }
} catch (Exception $e) { } catch (Exception $e) {
ray($e->getMessage()); send_internal_notification("Subscription webhook ($type) failed: " . $e->getMessage());
send_internal_notification('Subscription webhook failed: ' . $e->getMessage());
$webhook->update([ $webhook->update([
'status' => 'failed', 'status' => 'failed',
'failure_reason' => $e->getMessage(), 'failure_reason' => $e->getMessage(),