mirror of
				https://github.com/invoiceninja/invoiceninja.git
				synced 2025-11-04 05:07:31 -05:00 
			
		
		
		
	Add Billing Portal Login functionality
This commit is contained in:
		
							parent
							
								
									70fdc73b2d
								
							
						
					
					
						commit
						cfd33bd4fc
					
				
							
								
								
									
										162
									
								
								app/Livewire/BillingPortal/Authentication/Login.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								app/Livewire/BillingPortal/Authentication/Login.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,162 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Invoice Ninja (https://invoiceninja.com).
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @link https://github.com/invoiceninja/invoiceninja source repository
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @license https://www.elastic.co/licensing/elastic-license
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace App\Livewire\BillingPortal\Authentication;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Jobs\Mail\NinjaMailerJob;
 | 
				
			||||||
 | 
					use App\Jobs\Mail\NinjaMailerObject;
 | 
				
			||||||
 | 
					use App\Mail\Subscription\OtpCode;
 | 
				
			||||||
 | 
					use App\Models\ClientContact;
 | 
				
			||||||
 | 
					use App\Models\Subscription;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Cache;
 | 
				
			||||||
 | 
					use Livewire\Component;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Login extends Component
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public Subscription $subscription;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public array $context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ?string $email;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ?string $password;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ?int $otp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public array $state = [
 | 
				
			||||||
 | 
					        'otp' => false, // Use as preference. E-mail/password or OTP.
 | 
				
			||||||
 | 
					        'login_form' => false,
 | 
				
			||||||
 | 
					        'otp_form' => false,
 | 
				
			||||||
 | 
					        'initial_completed' => false,
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function initial()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->validateOnly('email', ['email' => 'required|bail|email:rfc|exists:client_contacts,email']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->state['initial_completed'] = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($this->state['otp']) {
 | 
				
			||||||
 | 
					            return $this->withOtp();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $this->withPassword();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function withPassword()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $contact = ClientContact::where('email', $this->email)
 | 
				
			||||||
 | 
					            ->where('company_id', $this->subscription->company_id)
 | 
				
			||||||
 | 
					            ->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($contact) {
 | 
				
			||||||
 | 
					            return $this->state['login_form'] = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->state['login_form'] = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $contact = $this->createClientContact();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        auth()->guard('contact')->loginUsingId($contact->id, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->dispatch('purchase.context', property: 'contact', value: $contact);
 | 
				
			||||||
 | 
					        $this->dispatch('purchase.next');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function withOtp()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $code = rand(100000, 999999);
 | 
				
			||||||
 | 
					        $email_hash = "subscriptions:otp:{$this->email}";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Cache::put($email_hash, $code, 600);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $cc = new ClientContact();
 | 
				
			||||||
 | 
					        $cc->email = $this->email;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $nmo = new NinjaMailerObject();
 | 
				
			||||||
 | 
					        $nmo->mailable = new OtpCode($this->subscription->company, $this->context['contact'] ?? null, $code);
 | 
				
			||||||
 | 
					        $nmo->company = $this->subscription->company;
 | 
				
			||||||
 | 
					        $nmo->settings = $this->subscription->company->settings;
 | 
				
			||||||
 | 
					        $nmo->to_user = $cc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        NinjaMailerJob::dispatch($nmo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (app()->environment('local')) {
 | 
				
			||||||
 | 
					            session()->flash('message', "[dev]: Your OTP is: {$code}");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->state['otp_form'] = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function handleOtp()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->validate([
 | 
				
			||||||
 | 
					            'otp' => 'required|numeric|digits:6',
 | 
				
			||||||
 | 
					            'email' => 'required|bail|email:rfc|exists:client_contacts,email',
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $code = Cache::get("subscriptions:otp:{$this->email}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($this->otp != $code) { //loose comparison prevents edge cases
 | 
				
			||||||
 | 
					            $errors = $this->getErrorBag();
 | 
				
			||||||
 | 
					            $errors->add('otp', ctrans('texts.invalid_code'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $contact = ClientContact::where('email', $this->email)
 | 
				
			||||||
 | 
					            ->where('company_id', $this->subscription->company_id)
 | 
				
			||||||
 | 
					            ->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($contact) {
 | 
				
			||||||
 | 
					            auth()->guard('contact')->loginUsingId($contact->id, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $this->dispatch('purchase.context', property: 'contact', value: $contact);
 | 
				
			||||||
 | 
					            $this->dispatch('purchase.next');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function handlePassword()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->validate([
 | 
				
			||||||
 | 
					            'email' => 'required|bail|email:rfc',
 | 
				
			||||||
 | 
					            'password' => 'required',
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $attempt = auth()->guard('contact')->attempt([
 | 
				
			||||||
 | 
					            'email' => $this->email,
 | 
				
			||||||
 | 
					            'password' => $this->password,
 | 
				
			||||||
 | 
					            'company_id' => $this->subscription->company_id,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($attempt) {
 | 
				
			||||||
 | 
					            $this->dispatch('purchase.next');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        session()->flash('message', 'These credentials do not match our records.');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function mount()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (auth()->guard('contact')->check()) {
 | 
				
			||||||
 | 
					            $this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
 | 
				
			||||||
 | 
					            $this->dispatch('purchase.next');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function render(): \Illuminate\View\View
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return view('billing-portal.v3.authentication.login');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,91 @@
 | 
				
			|||||||
 | 
					<div>
 | 
				
			||||||
 | 
					    @if (session()->has('message'))
 | 
				
			||||||
 | 
					        @component('portal.ninja2020.components.message')
 | 
				
			||||||
 | 
					            {{ session('message') }}
 | 
				
			||||||
 | 
					        @endcomponent
 | 
				
			||||||
 | 
					    @endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="my-4">
 | 
				
			||||||
 | 
					        <h1 class="text-3xl font-medium">{{ ctrans('texts.contact') }}</h1>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @if($state['initial_completed'] === false)
 | 
				
			||||||
 | 
					        <form wire:submit="initial">
 | 
				
			||||||
 | 
					            @csrf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <label for="email_address">
 | 
				
			||||||
 | 
					                <span class="input-label">{{ ctrans('texts.email_address') }}</span>
 | 
				
			||||||
 | 
					                <input wire:model="email" type="email" class="input w-full" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                @error('email')
 | 
				
			||||||
 | 
					                <p class="validation validation-fail block w-full" role="alert">
 | 
				
			||||||
 | 
					                    {{ $message }}
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					                @enderror
 | 
				
			||||||
 | 
					            </label>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <button 
 | 
				
			||||||
 | 
					                type="submit"
 | 
				
			||||||
 | 
					                class="button button-block bg-primary text-white mt-4">
 | 
				
			||||||
 | 
					                    {{ ctrans('texts.next') }}
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					        </form>
 | 
				
			||||||
 | 
					    @endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @if($state['login_form'])
 | 
				
			||||||
 | 
					    <form wire:submit="handlePassword" class="space-y-3">
 | 
				
			||||||
 | 
					        @csrf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					            <span class="input-label">{{ ctrans('texts.email_address') }}</span>
 | 
				
			||||||
 | 
					            <input wire:model="email" type="email" class="input w-full" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            @error('email')
 | 
				
			||||||
 | 
					            <p class="validation validation-fail block w-full" role="alert">
 | 
				
			||||||
 | 
					                {{ $message }}
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					            @enderror
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					            <span class="input-label">{{ ctrans('texts.password') }}</span>
 | 
				
			||||||
 | 
					            <input wire:model="password" type="password" class="input w-full" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            @error('password')
 | 
				
			||||||
 | 
					            <p class="validation validation-fail block w-full" role="alert">
 | 
				
			||||||
 | 
					                {{ $message }}
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					            @enderror
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <button 
 | 
				
			||||||
 | 
					            type="submit"
 | 
				
			||||||
 | 
					            class="button button-block bg-primary text-white mt-4">
 | 
				
			||||||
 | 
					                {{ ctrans('texts.next') }}
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					    @endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @if($state['otp_form'])
 | 
				
			||||||
 | 
					    <form wire:submit="handleOtp" class="space-y-3">
 | 
				
			||||||
 | 
					        @csrf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					            <span class="input-label">{{ ctrans('texts.code') }}</span>
 | 
				
			||||||
 | 
					            <input wire:model="otp" type="text" class="input w-full" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            @error('otp')
 | 
				
			||||||
 | 
					            <p class="validation validation-fail block w-full" role="alert">
 | 
				
			||||||
 | 
					                {{ $message }}
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					            @enderror
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <button 
 | 
				
			||||||
 | 
					            type="submit"
 | 
				
			||||||
 | 
					            class="button button-block bg-primary text-white mt-4">
 | 
				
			||||||
 | 
					                {{ ctrans('texts.next') }}
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					    @endif
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user