diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php index 67c50675f..7324ad694 100644 --- a/app/Console/Commands/Emails.php +++ b/app/Console/Commands/Emails.php @@ -24,7 +24,7 @@ use Illuminate\Mail\Message; use Illuminate\Notifications\Messages\MailMessage; use Mail; -use Str; +use Illuminate\Support\Str; use function Laravel\Prompts\confirm; use function Laravel\Prompts\select; @@ -62,7 +62,7 @@ public function handle() 'application-status-changed' => 'Application - Status Changed', 'backup-success' => 'Database - Backup Success', 'backup-failed' => 'Database - Backup Failed', - 'invitation-link' => 'Invitation Link', + // 'invitation-link' => 'Invitation Link', 'waitlist-invitation-link' => 'Waitlist Invitation Link', 'waitlist-confirmation' => 'Waitlist Confirmation', 'realusers-before-trial' => 'REAL - Registered Users Before Trial without Subscription', @@ -141,20 +141,20 @@ public function handle() $this->mail = (new BackupSuccess($backup, $db))->toMail(); $this->sendEmail(); break; - case 'invitation-link': - $user = User::all()->first(); - $invitation = TeamInvitation::whereEmail($user->email)->first(); - if (!$invitation) { - $invitation = TeamInvitation::create([ - 'uuid' => Str::uuid(), - 'email' => $user->email, - 'team_id' => 1, - 'link' => 'http://example.com', - ]); - } - $this->mail = (new InvitationLink($user))->toMail(); - $this->sendEmail(); - break; + // case 'invitation-link': + // $user = User::all()->first(); + // $invitation = TeamInvitation::whereEmail($user->email)->first(); + // if (!$invitation) { + // $invitation = TeamInvitation::create([ + // 'uuid' => Str::uuid(), + // 'email' => $user->email, + // 'team_id' => 1, + // 'link' => 'http://example.com', + // ]); + // } + // $this->mail = (new InvitationLink($user))->toMail(); + // $this->sendEmail(); + // break; case 'waitlist-invitation-link': $this->mail = new MailMessage(); $this->mail->view('emails.waitlist-invitation', [ diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index d15107c19..368f76f8f 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -21,7 +21,7 @@ protected function schedule(Schedule $schedule): void if (isDev()) { // $schedule->job(new ContainerStatusJob(Server::find(0)))->everyTenMinutes()->onOneServer(); // $schedule->command('horizon:snapshot')->everyMinute(); - // $schedule->job(new CleanupInstanceStuffsJob)->everyMinute(); + $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); // $schedule->job(new CheckResaleLicenseJob)->hourly(); // $schedule->job(new DockerCleanupJob)->everyOddHour(); // $this->instance_auto_update($schedule); @@ -29,7 +29,7 @@ protected function schedule(Schedule $schedule): void $this->check_resources($schedule); } else { $schedule->command('horizon:snapshot')->everyFiveMinutes(); - $schedule->job(new CleanupInstanceStuffsJob)->everyTenMinutes()->onOneServer(); + $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); $schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer(); $this->instance_auto_update($schedule); diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 913879c6b..0794d2507 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -3,21 +3,18 @@ namespace App\Http\Controllers; use App\Models\InstanceSettings; -use App\Models\Project; use App\Models\S3Storage; 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\Auth; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Facades\Http; +use Illuminate\Support\Str; use Throwable; -use Str; - class Controller extends BaseController { @@ -35,8 +32,15 @@ public function link() return redirect()->route('login'); } if (Hash::check($password, $user->password)) { + $invitation = TeamInvitation::whereEmail($email); + if ($invitation->exists()) { + $team = $invitation->first()->team; + $user->teams()->attach($team->id, ['role' => $invitation->first()->role]); + $invitation->delete(); + } else { + $team = $user->teams()->first(); + } Auth::login($user); - $team = $user->teams()->first(); session(['currentTeam' => $team]); return redirect()->route('dashboard'); } @@ -137,24 +141,20 @@ public function acceptInvitation() try { $invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail(); $user = User::whereEmail($invitation->email)->firstOrFail(); - if (is_null(auth()->user())) { - return redirect()->route('login'); - } if (auth()->user()->id !== $user->id) { abort(401); } - - $createdAt = $invitation->created_at; - $diff = $createdAt->diffInMinutes(now()); - if ($diff <= config('constants.invitation.link.expiration')) { + $invitationValid = $invitation->isValid(); + if ($invitationValid) { $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); + refreshSession($invitation->team); $invitation->delete(); return redirect()->route('team.index'); } else { - $invitation->delete(); abort(401); } } catch (Throwable $e) { + ray($e->getMessage()); throw $e; } } diff --git a/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php index ef84dca08..d73c644ef 100644 --- a/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -5,7 +5,7 @@ use App\Models\EnvironmentVariable; use Livewire\Component; use Visus\Cuid2\Cuid2; -use Str; +use Illuminate\Support\Str; class All extends Component { diff --git a/app/Http/Livewire/Team/InviteLink.php b/app/Http/Livewire/Team/InviteLink.php index b554e6575..c22c54a61 100644 --- a/app/Http/Livewire/Team/InviteLink.php +++ b/app/Http/Livewire/Team/InviteLink.php @@ -4,9 +4,13 @@ use App\Models\TeamInvitation; use App\Models\User; -use App\Notifications\TransactionalEmails\InvitationLink; +use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Support\Facades\Artisan; use Livewire\Component; use Visus\Cuid2\Cuid2; +use Illuminate\Support\Facades\Crypt; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Str; class InviteLink extends Component { @@ -20,53 +24,68 @@ public function mount() public function viaEmail() { - $this->generate_invite_link(isEmail: true); + $this->generate_invite_link(sendEmail: true); } - private function generate_invite_link(bool $isEmail = false) + public function viaLink() + { + $this->generate_invite_link(sendEmail: false); + } + private function generate_invite_link(bool $sendEmail = false) { try { - $uuid = new Cuid2(32); - $link = url('/') . config('constants.invitation.link.base_url') . $uuid; - - $user = User::whereEmail($this->email); - - if (!$user->exists()) { - return general_error_handler(that: $this, customErrorMessage: "$this->email must be registered first (or activate transactional emails to invite via email)."); - } - $member_emails = currentTeam()->members()->get()->pluck('email'); if ($member_emails->contains($this->email)) { return general_error_handler(that: $this, customErrorMessage: "$this->email is already a member of " . currentTeam()->name . "."); } + $uuid = new Cuid2(32); + $link = url('/') . config('constants.invitation.link.base_url') . $uuid; + $user = User::whereEmail($this->email)->first(); - $invitation = TeamInvitation::whereEmail($this->email); - - if ($invitation->exists()) { - $created_at = $invitation->first()->created_at; - $diff = $created_at->diffInMinutes(now()); - if ($diff <= config('constants.invitation.link.expiration')) { - return general_error_handler(that: $this, customErrorMessage: "Invitation already sent to $this->email and waiting for action."); + if (is_null($user)) { + $password = Str::password(); + $user = User::create([ + 'name' => Str::of($this->email)->before('@'), + 'email' => $this->email, + 'password' => Hash::make($password), + 'force_password_reset' => true, + ]); + $token = Crypt::encryptString("{$user->email}@@@$password"); + $link = route('auth.link', ['token' => $token]); + } + $invitation = TeamInvitation::whereEmail($this->email)->first(); + if (!is_null($invitation)) { + $invitationValid = $invitation->isValid(); + if ($invitationValid) { + return general_error_handler(that: $this, customErrorMessage: "Pending invitation already exists for $this->email."); } else { $invitation->delete(); } } - TeamInvitation::firstOrCreate([ + $invitation = TeamInvitation::firstOrCreate([ 'team_id' => currentTeam()->id, 'uuid' => $uuid, 'email' => $this->email, 'role' => $this->role, 'link' => $link, - 'via' => $isEmail ? 'email' : 'link', + 'via' => $sendEmail ? 'email' : 'link', ]); - if ($isEmail) { - $user->first()->notify(new InvitationLink); + if ($sendEmail) { + $mail = new MailMessage(); + $mail->view('emails.invitation-link', [ + 'team' => currentTeam()->name, + 'invitation_link' => $link, + ]); + $mail->subject('You have been invited to ' . currentTeam()->name . ' on ' . config('app.name') . '.'); + send_user_an_email($mail, $this->email); $this->emit('success', 'Invitation sent via email successfully.'); + $this->emit('refreshInvitations'); + return; } else { $this->emit('success', 'Invitation link generated.'); + $this->emit('refreshInvitations'); } - $this->emit('refreshInvitations'); } catch (\Throwable $e) { $error_message = $e->getMessage(); if ($e->getCode() === '23505') { @@ -75,9 +94,4 @@ private function generate_invite_link(bool $isEmail = false) return general_error_handler(err: $e, that: $this, customErrorMessage: $error_message); } } - - public function viaLink() - { - $this->generate_invite_link(); - } } diff --git a/app/Http/Livewire/Team/Member.php b/app/Http/Livewire/Team/Member.php index df8fcd7af..34b8e6bde 100644 --- a/app/Http/Livewire/Team/Member.php +++ b/app/Http/Livewire/Team/Member.php @@ -3,6 +3,7 @@ namespace App\Http\Livewire\Team; use App\Models\User; +use Illuminate\Support\Facades\Cache; use Livewire\Component; class Member extends Component @@ -24,6 +25,10 @@ public function makeReadonly() public function remove() { $this->member->teams()->detach(currentTeam()); + Cache::forget("team:{$this->member->id}"); + Cache::remember('team:' . $this->member->id, 3600, function() { + return $this->member->teams()->first(); + }); $this->emit('reloadWindow'); } } diff --git a/app/Http/Livewire/Waitlist/Index.php b/app/Http/Livewire/Waitlist/Index.php index d2ce6fe19..86f8d8929 100644 --- a/app/Http/Livewire/Waitlist/Index.php +++ b/app/Http/Livewire/Waitlist/Index.php @@ -6,7 +6,7 @@ use App\Models\User; use App\Models\Waitlist; use Livewire\Component; -use Str; +use Illuminate\Support\Str; class Index extends Component { diff --git a/app/Http/Middleware/IsBoardingFlow.php b/app/Http/Middleware/IsBoardingFlow.php index 5858fe191..6e820f483 100644 --- a/app/Http/Middleware/IsBoardingFlow.php +++ b/app/Http/Middleware/IsBoardingFlow.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; +use Illuminate\Support\Str; class IsBoardingFlow { @@ -17,6 +18,9 @@ public function handle(Request $request, Closure $next): Response { // ray()->showQueries()->color('orange'); if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) { + if (Str::startsWith($request->path(), 'invitations')) { + return $next($request); + } return redirect('boarding'); } return $next($request); diff --git a/app/Http/Middleware/IsSubscriptionValid.php b/app/Http/Middleware/IsSubscriptionValid.php index a9354e982..5774e5190 100644 --- a/app/Http/Middleware/IsSubscriptionValid.php +++ b/app/Http/Middleware/IsSubscriptionValid.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; +use Illuminate\Support\Str; class IsSubscriptionValid { @@ -31,6 +32,9 @@ public function handle(Request $request, Closure $next): Response if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) { // ray('SubscriptionValid Middleware'); if (!in_array($request->path(), allowedPathsForUnsubscribedAccounts())) { + if (Str::startsWith($request->path(), 'invitations')) { + return $next($request); + } return redirect('subscription'); } else { return $next($request); diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php index f9d88cd4d..81a6963ea 100644 --- a/app/Jobs/CleanupInstanceStuffsJob.php +++ b/app/Jobs/CleanupInstanceStuffsJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Models\TeamInvitation; use App\Models\Waitlist; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; @@ -32,7 +33,12 @@ public function handle(): void } catch (\Throwable $e) { send_internal_notification('CleanupInstanceStuffsJob failed with error: ' . $e->getMessage()); ray($e->getMessage()); - throw $e; + } + try { + $this->cleanup_invitation_link(); + } catch (\Throwable $e) { + send_internal_notification('CleanupInstanceStuffsJob failed with error: ' . $e->getMessage()); + ray($e->getMessage()); } } @@ -43,4 +49,11 @@ private function cleanup_waitlist() $item->delete(); } } + private function cleanup_invitation_link() + { + $invitation = TeamInvitation::all(); + foreach ($invitation as $item) { + $item->isValid(); + } + } } diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 5fb0655b0..100c33e51 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -17,7 +17,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; -use Str; +use Illuminate\Support\Str; class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted { diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php index 37f1cf381..23d49c40e 100644 --- a/app/Jobs/SendMessageToTelegramJob.php +++ b/app/Jobs/SendMessageToTelegramJob.php @@ -9,7 +9,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; -use Str; +use Illuminate\Support\Str; class SendMessageToTelegramJob implements ShouldQueue, ShouldBeEncrypted { diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php index a5c382b3b..75a726f9f 100644 --- a/app/Models/TeamInvitation.php +++ b/app/Models/TeamInvitation.php @@ -19,4 +19,13 @@ public function team() { return $this->belongsTo(Team::class); } + public function isValid() { + $createdAt = $this->created_at; + $diff = $createdAt->diffInMinutes(now()); + if ($diff <= config('constants.invitation.link.expiration')) { + return true; + } else { + $this->delete(); + } + } } diff --git a/app/Models/User.php b/app/Models/User.php index 7df134408..aba05acf3 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,10 +4,10 @@ use App\Notifications\Channels\SendsEmail; use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword; -use Cache; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Facades\Cache; use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Sanctum\HasApiTokens; @@ -61,7 +61,7 @@ public function sendPasswordResetNotification($token): void public function isAdmin() { - return $this->pivot->role === 'admin' || $this->pivot->role === 'owner'; + return data_get($this->pivot,'role') === 'admin' || data_get($this->pivot,'role') === 'owner'; } public function isAdminFromSession() @@ -78,7 +78,8 @@ public function isAdminFromSession() if ($is_part_of_root_team && $is_admin_of_root_team) { return true; } - $role = $teams->where('id', session('currentTeam')->id)->first()->pivot->role; + $team = $teams->where('id', session('currentTeam')->id)->first(); + $role = data_get($team,'pivot.role'); return $role === 'admin' || $role === 'owner'; } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 5c504b243..ffd429c58 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -11,6 +11,7 @@ use Illuminate\Database\QueryException; use Illuminate\Mail\Message; use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Route; diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php index ae3506782..ac41b1a1d 100644 --- a/bootstrap/helpers/subscriptions.php +++ b/bootstrap/helpers/subscriptions.php @@ -56,7 +56,7 @@ function isSubscriptionActive() } $subscription = $team?->subscription; - if (!$subscription) { + if (is_null($subscription)) { return false; } if (isLemon()) { diff --git a/database/migrations/2023_08_22_071060_change_invitation_link_length.php b/database/migrations/2023_08_22_071060_change_invitation_link_length.php new file mode 100644 index 000000000..4efb03351 --- /dev/null +++ b/database/migrations/2023_08_22_071060_change_invitation_link_length.php @@ -0,0 +1,29 @@ +text('link')->change(); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('team_invitations', function (Blueprint $table) { + $table->string('link')->change(); + }); + } +}; diff --git a/resources/views/emails/invitation-link.blade.php b/resources/views/emails/invitation-link.blade.php index 05c36bb8a..ae72745ba 100644 --- a/resources/views/emails/invitation-link.blade.php +++ b/resources/views/emails/invitation-link.blade.php @@ -6,6 +6,5 @@ If you have any questions, please contact the team owner.

-If it was not you who requested this invitation, please ignore this email, or instantly revoke the invitation by clicking [here]({{ $invitation_link }}/revoke). - +If it was not you who requested this invitation, please ignore this email. diff --git a/resources/views/subscription/index.blade.php b/resources/views/subscription/index.blade.php index e13d856de..d36f26c81 100644 --- a/resources/views/subscription/index.blade.php +++ b/resources/views/subscription/index.blade.php @@ -1,7 +1,34 @@ @if ($settings->is_resale_license_active) -
-
+ @if (auth()->user()->isAdminFromSession()) +
+
+
+

Subscription

+ +
+
+ Currently active team: {{ session('currentTeam.name') }} +
+ @if (request()->query->get('cancelled')) +
+ + + + Something went wrong with your subscription. Please try again or contact + support. +
+ @endif + @if (config('subscription.provider') !== null) + + @endif +
+
+ @else +

Subscription

@@ -10,22 +37,10 @@ Currently active team: {{ session('currentTeam.name') }}
- @if (request()->query->get('cancelled')) -
- - - - Something went wrong with your subscription. Please try again or contact support. -
- @endif - @if (config('subscription.provider') !== null) - - @endif +
You are not an admin or have been removed from this team. If this does not make sense, please contact us.
-
+ @endif @else -
Resale license is not active. Please contact your instance admin.
+
Resale license is not active. Please contact your instance admin.
@endif