Merge pull request #3766 from beganovich/v2-2805-client-signup

Client registration
This commit is contained in:
David Bomba 2020-06-23 07:28:01 +10:00 committed by GitHub
commit b5bb00482b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 512 additions and 3 deletions

View File

@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Factory\ClientContactFactory;
use App\Factory\ClientFactory;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\RegisterRequest;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class ContactRegisterController extends Controller
{
public function __construct()
{
$this->middleware(['guest', 'contact.register']);
}
public function showRegisterForm(string $company_key)
{
$company = Company::where('company_key', $company_key)->firstOrFail();
return render('auth.register', compact(['company']));
}
public function register(RegisterRequest $request)
{
$request->merge(['company' => $request->company()]);
$client = $this->getClient($request->all());
$client_contact = $this->getClientContact($request->all(), $client);
Auth::guard('contact')->login($client_contact, true);
return redirect()->route('client.dashboard');
}
private function getClient(array $data)
{
$client = ClientFactory::create($data['company']->id, $data['company']->owner()->id);
$client->fill($data);
$client->save();
return $client;
}
public function getClientContact(array $data, Client $client)
{
$client_contact = ClientContactFactory::create($data['company']->id, $data['company']->owner()->id);
$client_contact->fill($data);
$client_contact->client_id = $client->id;
$client_contact->is_primary = true;
$client_contact->password = Hash::make($data['password']);
$client_contact->save();
return $client_contact;
}
}

View File

@ -107,5 +107,6 @@ class Kernel extends HttpKernel
'web_db' => \App\Http\Middleware\SetWebDb::class,
'api_db' => \App\Http\Middleware\SetDb::class,
'locale' => \App\Http\Middleware\Locale::class,
'contact.register' => \App\Http\Middleware\ContactRegister::class,
];
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Middleware;
use App\Models\Company;
use Closure;
class ContactRegister
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
/**
* Notes:
*
* 1. If request supports subdomain (for hosted) check domain and continue request.
* 2. If request doesn't support subdomain and doesn' have company_key, abort
* 3. firstOrFail() will abort with 404 if company with company_key wasn't found.
* 4. Abort if setting isn't enabled.
*/
if ($request->subdomain) {
$company = Company::where('subdomain', $request->subdomain)->firstOrFail();
abort_unless($company->getSetting('enable_client_registration'), 404);
return $next($request);
}
abort_unless($request->company_key, 404);
$company = Company::where('company_key', $request->company_key)->firstOrFail();
abort_unless($company->getSetting('client_can_register'), 404);
return $next($request);
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\Requests\ClientPortal;
use App\Models\Company;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'first_name' => ['required', 'string', 'max:255'],
'last_name' => ['required', 'string', 'max:255'],
'phone' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:client_contacts'],
'password' => ['required', 'string', 'min:6', 'confirmed'],
];
}
public function company()
{
if ($this->subdomain) {
return Company::where('subdomain', $this->subdomain)->firstOrFail();
}
if ($this->company_key) {
return Company::where('company_key', $this->company_key)->firstOrFail();
}
abort(404);
}
}

View File

@ -84,6 +84,7 @@ class ClientContact extends Authenticatable implements HasLocalePreference
'custom_value4',
'email',
'is_primary',
'client_id',
];
public function getEntityType()

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"/js/app.js": "/js/app.js?id=baf7fef12d5e65c3d9ff",
"/css/app.css": "/css/app.css?id=0c7bff0fb63b08940c81",
"/css/app.css": "/css/app.css?id=369c7335c317e8ac0212",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=0632d6281202800e0921",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=d7e708d66a9c769b4c6e",
"/js/clients/payment_methods/authorize-ach.js": "/js/clients/payment_methods/authorize-ach.js?id=9e6495d9ae236b3cb5ad",

View File

@ -3209,6 +3209,9 @@ return [
'payment_failed_subject' => 'Payment failed for Client :client',
'payment_failed_body' => 'A payment made by client :client failed with message :message',
'register' => 'Register',
'register_label' => 'Create your account in seconds',
'password_confirmation' => 'Confirm your password',
'verification' => 'Verification',
'complete_your_bank_account_verification' => 'Before using bank account they must be verified.',

View File

@ -0,0 +1,73 @@
<!-- Client personal address -->
<h3 class="text-lg font-medium leading-6 text-gray-900 mt-8">{{ ctrans('texts.personal_address') }}</h3>
<p class="mt-1 text-sm leading-5 text-gray-500">
{{ ctrans('texts.enter_your_personal_address') }}
</p>
<div class="shadow overflow-hidden rounded mt-4">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-4">
<label for="address1" class="input-label">{{ ctrans('texts.address1') }}</label>
<input id="address1" class="input w-full" name="address1" />
@error('address1')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="address2" class="input-label">{{ ctrans('texts.address2') }}</label>
<input id="address2" class="input w-full" name="address2" />
@error('address2')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="city" class="input-label">{{ ctrans('texts.city') }}</label>
<input id="city" class="input w-full" name="city" />
@error('city')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="state" class="input-label">{{ ctrans('texts.state') }}</label>
<input id="state" class="input w-full" name="state" />
@error('state')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="postal_code" class="input-label">{{ ctrans('texts.postal_code') }}</label>
<input id="postal_code" class="input w-full" name="postal_code" />
@error('postal_code')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="country" class="input-label">{{ ctrans('texts.country') }}</label>
<select id="country" class="input w-full form-select" name="country">
@foreach(App\Utils\TranslationHelper::getCountries() as $country)
<option value="{{ $country->id }}">
{{ $country->iso_3166_2 }} ({{ $country->name }})
</option>
@endforeach
</select>
@error('country')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,90 @@
<!-- Personal info, first name, last name, e-mail address .. -->
<h3 class="text-lg font-medium leading-6 text-gray-900 mt-8">{{ ctrans('texts.profile') }}</h3>
<p class="mt-1 text-sm leading-5 text-gray-500">
{{ ctrans('texts.client_information_text') }}
</p>
<div class="shadow overflow-hidden rounded mt-4">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3">
<section class="flex items-center">
<label for="first_name" class="input-label">{{ ctrans('texts.first_name') }}</label>
<section class="text-red-400 ml-1 text-sm">*</section>
</section>
<input id="first_name" class="input w-full" name="first_name" />
@error('first_name')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<section class="flex items-center">
<label for="last_name" class="input-label">{{ ctrans('texts.last_name') }}</label>
<section class="text-red-400 ml-1 text-sm">*</section>
</section>
<input id="last_name" class="input w-full" name="last_name" />
@error('last_name')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-4">
<section class="flex items-center">
<label for="email_address" class="input-label">{{ ctrans('texts.email_address') }}</label>
<section class="text-red-400 ml-1 text-sm">*</section>
</section>
<input id="email_address" class="input w-full" type="email" name="email" />
@error('email')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-4">
<section class="flex items-center">
<label for="phone" class="input-label">{{ ctrans('texts.phone') }}</label>
<section class="text-red-400 ml-1 text-sm">*</section>
</section>
<input id="phone" class="input w-full" name="phone" />
@error('phone')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-6 lg:col-span-3">
<section class="flex items-center">
<label for="password" class="input-label">{{ ctrans('texts.password') }}</label>
<section class="text-red-400 ml-1 text-sm">*</section>
</section>
<input id="password" class="input w-full" name="password" type="password" />
@error('password')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3 lg:col-span-3">
<section class="flex items-center">
<label for="password_confirmation" class="input-label">{{ ctrans('texts.confirm_password') }}</label>
<section class="text-red-400 ml-1 text-sm">*</section>
</section>
<input id="state" class="input w-full" name="password_confirmation" type="password" />
@error('password_confirmation')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,30 @@
<a class="button-link" x-on:click="{{ $property }} = true" href="#">{{ ctrans("texts.$property") }}</a>
<span class="text-gray-300">/</span>
<div x-show="{{ $property }}" class="fixed bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center">
<div x-show="{{ $property }}" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="fixed inset-0 transition-opacity">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<div x-show="{{ $property }}" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" class="relative bg-white rounded-lg px-4 pt-5 pb-4 overflow-hidden shadow-xl transform transition-all sm:max-w-lg sm:w-full sm:p-6" role="dialog" aria-modal="true" aria-labelledby="modal-headline">
<div class="hidden sm:block absolute top-0 right-0 pt-4 pr-4">
<button @click="{{ $property }} = false" type="button" class="text-gray-400 hover:text-gray-500 focus:outline-none focus:text-gray-500 transition ease-in-out duration-150" aria-label="Close">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
{{ $title }}
</h3>
<div class="mt-2">
<p class="text-sm leading-5 text-gray-500">
{{ $content }}
</p>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,74 @@
<!-- Client shipping address -->
<h3 class="text-lg font-medium leading-6 text-gray-900 mt-8">{{ ctrans('texts.shipping_address') }}</h3>
<p class="mt-1 text-sm leading-5 text-gray-500">
{{ ctrans('texts.enter_your_shipping_address') }}
</p>
<div class="shadow overflow-hidden rounded mt-4">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-4">
<label for="shipping_address1" class="input-label">{{ ctrans('texts.shipping_address1') }}</label>
<input id="shipping_address1" class="input w-full" name="shipping_address1" />
@error('shipping_address1')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="shipping_address2" class="input-label">{{ ctrans('texts.shipping_address2') }}</label>
<input id="shipping_address2" class="input w-full" name="shipping_address2" />
@error('shipping_address2')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="shipping_city" class="input-label">{{ ctrans('texts.shipping_city') }}</label>
<input id="shipping_city" class="input w-full" name="shipping_city" />
@error('shipping_city')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="shipping_state" class="input-label">{{ ctrans('texts.shipping_state') }}</label>
<input id="shipping_state" class="input w-full" name="shipping_state" />
@error('shipping_state')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="shipping_postal_code" class="input-label">{{ ctrans('texts.shipping_postal_code') }}</label>
<input id="shipping_postal_code" class="input w-full" name="shipping_postal_code" />
@error('shipping_postal_code')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-4 sm:col-span-2">
<label for="shipping_country" class="input-label">{{ ctrans('texts.shipping_country') }}</label>
<select id="shipping_country" class="input w-full form-select" name="shipping_country">
@foreach(App\Utils\TranslationHelper::getCountries() as $country)
<option {{ $country == isset(auth()->user()->client->shipping_country->id) ? 'selected' : null }} value="{{ $country->id }}">
{{ $country->iso_3166_2 }}
({{ $country->name }})
</option>
@endforeach
</select>
@error('country')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,32 @@
<!-- Name, website -->
<h3 class="text-lg font-medium leading-6 text-gray-900 mt-8">{{ ctrans('texts.website') }}</h3>
<p class="mt-1 text-sm leading-5 text-gray-500">
{{ ctrans('texts.make_sure_use_full_link') }}
</p>
<div class="shadow overflow-hidden rounded mt-4">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3">
<label for="street" class="input-label">{{ ctrans('texts.name') }}</label>
<input id="name" class="input w-full" name="name" />
@error('name')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="website" class="input-label">{{ ctrans('texts.website') }}</label>
<input id="website" class="input w-full" name="last_name" />
@error('website')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,44 @@
@extends('portal.ninja2020.layout.clean', ['custom_body_class' => 'bg-gray-100'])
@section('meta_title', ctrans('texts.register'))
@section('body')
<div class="grid lg:grid-cols-12 py-8">
<div class="col-span-4 col-start-5">
<div class="flex justify-center">
<img class="h-32 w-auto" src="{{ $company->present()->logo() }}" alt="{{ ctrans('texts.logo') }}">
</div>
<h1 class="text-center text-3xl mt-8">{{ ctrans('texts.register') }}</h1>
<p class="block text-center text-gray-600">{{ ctrans('texts.register_label') }}</p>
<form action="{{ route('client.register', request()->route('company_key')) }}" method="POST" x-data="{ more: false }">
@csrf
@include('portal.ninja2020.auth.includes.register.personal_information')
<span class="block mt-4 text-gray-800 hover:text-gray-900 text-right cursor-pointer" x-on:click="more = !more">{{ ctrans('texts.more_fields') }}</span>
<div x-show="more">
@include('portal.ninja2020.auth.includes.register.website')
@include('portal.ninja2020.auth.includes.register.personal_address')
@include('portal.ninja2020.auth.includes.register.shipping_address')
</div>
<div class="flex justify-between items-center mt-8">
<span class="inline-flex items-center" x-data="{ terms_of_service: false, privacy_policy: false }">
@if(!empty($company->settings->client_signup_terms) || !empty($company->settings->client_signup_privacy_policy))
<input type="checkbox" name="terms" class="form-checkbox mr-2 cursor-pointer" checked>
<span class="text-sm text-gray-800">
{{ ctrans('texts.i_agree') }}
@endif
@includeWhen(!empty($company->settings->client_signup_terms), 'portal.ninja2020.auth.includes.register.popup', ['property' => 'terms_of_service', 'title' => ctrans('texts.terms_of_service'), 'content' => $company->settings->client_signup_terms])
@includeWhen(!empty($company->settings->client_signup_privacy_policy), 'portal.ninja2020.auth.includes.register.popup', ['property' => 'privacy_policy', 'title' => ctrans('texts.privacy_policy'), 'content' => $company->settings->client_signup_privacy_policy])
</span>
</span>
<button class="button button-primary">{{ ctrans('texts.save') }}</button>
</div>
</form>
</div>
</div>
@endsection

View File

@ -44,6 +44,7 @@
<!-- Scripts -->
<script src="{{ mix('js/app.js') }}" defer></script>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
@ -60,7 +61,7 @@
</head>
<body class="antialiased">
<body class="antialiased {{ $custom_body_class ?? '' }}">
@yield('body')
</body>

View File

@ -7,6 +7,9 @@ Route::get('client', 'Auth\ContactLoginController@showLoginForm')->name('client.
Route::get('client/login', 'Auth\ContactLoginController@showLoginForm')->name('client.login')->middleware('locale');
Route::post('client/login', 'Auth\ContactLoginController@login')->name('client.login.submit');
Route::get('client/register/{company_key?}', 'Auth\ContactRegisterController@showRegisterForm')->name('client.register');
Route::post('client/register/{company_key?}', 'Auth\ContactRegisterController@register');
Route::get('client/password/reset', 'Auth\ContactForgotPasswordController@showLinkRequestForm')->name('client.password.request')->middleware('locale');
Route::post('client/password/email', 'Auth\ContactForgotPasswordController@sendResetLinkEmail')->name('client.password.email')->middleware('locale');
Route::get('client/password/reset/{token}', 'Auth\ContactResetPasswordController@showResetForm')->name('client.password.reset')->middleware('locale');

1
tailwind.config.js vendored
View File

@ -5,6 +5,7 @@ module.exports = {
'./resources/views/portal/ninja2020/**/*.blade.php',
'./resources/views/email/**/*.blade.php',
'./resources/views/themes/ninja2020/**/*.blade.php',
'./resources/views/auth/**/*.blade.php',
],
theme: {
extend: {