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:
parent
0dbb8b4420
commit
e7c0c26b32
@ -6,6 +6,7 @@
|
||||
USERID=
|
||||
GROUPID=
|
||||
############################################################################################################
|
||||
APP_NAME=Coolify-localhost
|
||||
APP_ID=development
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
|
@ -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')) {
|
||||
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);
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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') {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
32
database/migrations/2023_08_22_071054_add_stripe_reasons.php
Normal file
32
database/migrations/2023_08_22_071054_add_stripe_reasons.php
Normal 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');
|
||||
});
|
||||
}
|
||||
};
|
@ -51,6 +51,11 @@ class="text-xs normal-case hover:no-underline btn btn-sm bg-coollabs-gradient">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="mb-4 font-medium text-red-600">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{{ Illuminate\Mail\Markdown::parse('---') }}
|
||||
|
||||
Thank you.<br>
|
||||
Thank you,<br>
|
||||
{{ config('app.name') ?? 'Coolify' }}
|
||||
|
||||
{{ Illuminate\Mail\Markdown::parse('[Contact Support](https://docs.coollabs.io)') }}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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.
|
||||
|
||||
|
@ -1,19 +1,4 @@
|
||||
<x-emails.layout>
|
||||
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}})
|
||||
</x-emails.layout>
|
||||
|
||||
|
@ -1,5 +1,20 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Unauthorized'))
|
||||
@section('code', '401')
|
||||
@section('message', __('Unauthorized'))
|
||||
@extends('layouts.base')
|
||||
<div class="min-h-screen hero">
|
||||
<div class="text-center hero-content">
|
||||
<div class="">
|
||||
<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>
|
||||
|
@ -1,5 +1,20 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Forbidden'))
|
||||
@section('code', '403')
|
||||
@section('message', __($exception->getMessage() ?: 'Forbidden'))
|
||||
@extends('layouts.base')
|
||||
<div class="min-h-screen hero">
|
||||
<div class="text-center hero-content">
|
||||
<div class="">
|
||||
<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>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<x-layout>
|
||||
<div class="min-h-screen hero">
|
||||
@extends('layouts.base')
|
||||
<div class="min-h-screen hero">
|
||||
<div class="text-center hero-content">
|
||||
<div class="">
|
||||
<p class="font-mono text-6xl font-semibold text-warning">404</p>
|
||||
@ -9,7 +9,7 @@
|
||||
</p>
|
||||
<div class="flex items-center justify-center mt-10 gap-x-6">
|
||||
<a href="/">
|
||||
<x-forms.button isHighlighted>Go back home</x-forms.button>
|
||||
<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
|
||||
@ -18,5 +18,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-layout>
|
||||
</div>
|
||||
|
@ -1,21 +1,20 @@
|
||||
<x-layout>
|
||||
<div class="min-h-screen hero">
|
||||
@extends('layouts.base')
|
||||
<div class="min-h-screen hero">
|
||||
<div class="text-center hero-content">
|
||||
<div class="">
|
||||
<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 couldn’t find the page you’re looking
|
||||
for.
|
||||
</p>
|
||||
<div class="flex items-center justify-center mt-10 gap-x-6">
|
||||
<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
|
||||
back home</a>
|
||||
<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">→</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-layout>
|
||||
</div>
|
||||
|
@ -1,5 +1,19 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Too Many Requests'))
|
||||
@section('code', '429')
|
||||
@section('message', __('Too Many Requests'))
|
||||
@extends('layouts.base')
|
||||
<div class="min-h-screen hero">
|
||||
<div class="text-center hero-content">
|
||||
<div class="">
|
||||
<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">→</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,23 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Server Error'))
|
||||
@section('code', '500')
|
||||
@section('message', __('Server Error'))
|
||||
@extends('layouts.base')
|
||||
<div class="min-h-screen hero ">
|
||||
<div class="text-center hero-content">
|
||||
<div>
|
||||
<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">→</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,17 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Service Unavailable'))
|
||||
@section('code', '503')
|
||||
@section('message', __('Service Unavailable'))
|
||||
@extends('layouts.base')
|
||||
<div class="min-h-screen hero ">
|
||||
<div class="text-center hero-content">
|
||||
<div>
|
||||
<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">→</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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>
|
@ -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>
|
@ -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']);
|
||||
|
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user