In questo tutorial vedremo come aggiungere la profilazione dei clienti ad un e-commerce in Laravel.

Creiamo la tabella customers definendo dal terminale il file di migrazione ed il modello associato.

php artisan make:model Customer --migration

Il profilo del cliente dovrà avere il nome, il cognome, l'e-mail,la password e un flag che indicherà se l'utente ha confermato il suo account. Modifichiamo quindi il file della migrazione come segue.

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateCustomersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('customers', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('firstname', 255);
            $table->string('lastname', 255);
            $table->string('email', 255);
            $table->string('password', 255);
            $table->tinyInteger('verified', false, true);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('customers');
    }
}

Lanciamo la migrazione:

php artisan migrate

Ora possiamo definire il modello Customer di riferimento.

namespace App;

use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
    protected $table = 'customers';
    protected $primaryKey = 'id';
    public $incrementing = true;
    protected $fillable = ['firstname', 'lastname', 'email','password','verified'];

}

Abbiamo deciso di non usare il modello User predefinito di Laravel al fine di poter implementare da zero un meccanismo di autenticazione e verifica dell'account.

Come prima cosa dobbiamo modificare la durata di una sessione in Laravel cambiando il valore espresso in secondi della variabile di ambiente SESSION_LIFETIME presente nel file .env in questo modo:

SESSION_LIFETIME=43200

Il valore ora indica che il cookie di sessione avrà una durata complessiva di 30 giorni. Per applicare le modifiche, digitiamo dal terminale:

php artisan config:clear

Lo step successivo consiste nell'implementare la logica di registrazione sul sito con la verifica dell'indirizzo e-mail.

Per farlo dobbiamo attivare un gateway SMTP per l'invio delle e-mail. In questo caso useremo Mailtrap, che ci fornisce già i parametri di configurazione da inserire nel file .env di Laravel.

MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=username
MAIL_PASSWORD=password
MAIL_ENCRYPTION=tls

Per applicare le modifiche, digitiamo dal terminale:

php artisan config:clear

Ora dobbiamo creare una classe di tipo Mailable che implementerà la logica dell'invio dell'e-mail con relativov template. Digitiamo dal terminale:

php artisan make:mail ConfirmationEmail

La classe dovrà accettare come unico parametro del costruttore l'URL completo per la verifica dell'indirizzo e-mail.

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class ConfirmationEmail extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct( $url )
    {
        $this->confirmation_url = $url;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return
            $this->from('phpecommerce@localhost')->
        subject('Confirm your e-mail address')->
        view('email.confirmation', [
            'confirmation_url' => $this->confirmation_url
        ]);
    }
}

Possiamo quindi creare il template Blade in resources/views/email:

<p>Dear customer,<br>
    click on the following link to confirm your e-mail address.</p>

<p><a href="{{ $confirmation_url }}">Confirm</a></p>

Poiché l'URL conterrà il token di verifica, dobbiamo creare la tabella dei token creando un nuovo modello ed una nuova migrazione.

php artisan make:model VerificationToken --migration

Definiamo il file di migrazione come segue:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateVerificationTokensTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('verification_tokens', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->bigInteger('customer_id');
            $table->string('value', 255);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('verification_tokens');
    }
}

Ciascun token è collegato al profilo cliente tramite il campo customer_id. Quindi definiamo il modello corrispondente:

namespace App;

use Illuminate\Database\Eloquent\Model;

class VerificationToken extends Model
{
    protected $table = 'verification_tokens';
    protected $primaryKey = 'id';
    public $incrementing = true;
    protected $fillable = ['customer_id', 'value'];
}

Lanciamo la migrazione:

php artisan migrate

Ora possiamo creare il metodo di registrazione nel nostro controller AJAX. Scegliamo AJAX per dare all'utente un feedback immediato.

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Mail\ConfirmationEmail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use App\Customer;
use App\VerificationToken;

class AjaxController extends Controller
{
        public function registerCustomer(Request $request)
    {
        $messages = [
            'required' => 'Required field.',
            'email' => 'Invalid e-mail address.'
        ];

        $validator = Validator::make($request->all(), [
            'register_firstname' => 'required',
            'register_lastname' => 'required',
            'register_email' => 'required|email:rfc',
            'register_password' => 'required'
        ], $messages);

        if ($validator->fails()) {
            return response()->json($validator->messages());
        }

        $firstname = $request->get('register_firstname');
        $lastname = $request->get('register_lastname');
        $email = $request->get('register_email');
        $password = Hash::make($request->get('register_password'));

        $customer_data = compact('firstname', 'lastname', 'email', 'password');
        $customer = new Customer($customer_data);

        $customer->save();

        $confirmation_token = Str::random(60);

        $verification_token = new VerificationToken([
            'customer_id' => $customer->id,
            'value' => $confirmation_token
        ]);

        $verification_token->save();

        $confirmation_url = env('APP_URL') . '/confirm/' . $customer->id . '/' . $confirmation_token;

        Mail::to($email)->send(new ConfirmationEmail($confirmation_url));

        return response()->json(['success' => 'We sent you an e-mail with the instructions to activate your account.']);
    }
}

L'URL che viene inviato tramite e-mail se la validazione ha avuto successo è composto dall'ID del cliente e dal token di verifica. Ora il profilo utente viene salvato nel database ma non è ancora attivato.

Per renderlo attivo l'utente deve cliccare sul link inviato via e-mail. Definiamo quindi tale logica in un metodo specifico del controller principale del nostro sito.

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Customer;
use App\VerificationToken;
use Illuminate\Http\Request;

class ShopController extends Controller
{
        public function confirm($id, $token)
    {
        $customer = Customer::find($id);
        $customer_token = VerificationToken::where('value', $token)->first();
        $is_valid_token = ( $customer && $customer_token );

        if(!$is_valid_token) {
            return redirect()->route('register')->with('error', 'Invalid parameter.');
        }

        $customer_token->delete();
        $customer->verified = 1;
        $customer->save();

        return redirect()->route('register')->with('success', 'Account activated. You can now log in.');

    }
}

Il token viene eliminato a conferma effettuata e il profilo del cliente viene aggiornato impostando il flag verified a 1. Se il token non è valido l'utente viene reindirizzato alla pagina di registrazione con un messaggio di errore. Al contrario, il messaggio confermerà l'avvenuta attivazione dell'account.

@if (session('error'))
            <div class="alert alert-danger mt-4 mb-4">
                {{ session('error') }}
            </div>
        @endif
        @if (session('success'))
            <div class="alert alert-success mt-4 mb-4">
                {{ session('success') }}
            </div>
        @endif

L'utente può ora effettuare il login. Definiamo un nuovo metodo nel nostro controller AJAX.

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use App\Customer;

class AjaxController extends Controller
{
        public function loginCustomer(Request $request)
    {
        $email = $request->get('email');
        $password = $request->get('password');

        $customer = Customer::where('email', $email)->first();


        if(!$customer || !Hash::check($password, $customer->password)) {
            return response()->json(['error' => 'Invalid login.']);
        }

        session()->put('logged_in', '1');
        session()->put('customer_id', $customer->id);

        return response()->json(['success' => true]);
    }
}

La verifica avviene in due fasi. Nella prima si cerca una corrispondenza nella tabella dei clienti usando l'e-mail del cliente, nella seconda si verifica che l'hash della password fornita corrisponda all'hash salvato nel database. Se la verifica ha successo, vengono create due variabili di sessione che indicano che il cliente è loggato e conservano il suo ID.

Lato client definiremo il seguente codice in jQuery.

$.ajaxSetup({
        headers: {
            'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
        }
    });
    
if( $( "#login-form" ).length ) {
        $( "#login-form" ).on( "submit", function( e ) {
            e.preventDefault();
            var $form = $( this );
            $form.find( ".alert" ).remove();

            $.post( "/ajax/login", $form.serialize(), function ( res ) {
                if( res.success ) {
                    window.location = "/profile";
                } else {
                    $form.append( '<div class="alert alert-danger mt-4">' + res.error + '</div>' );
                }
            });

        });
    }    

In caso di avvenuto login, l'utente viene reindirizzato nella sua pagina personale.

Un aspetto importante della profilazione è che ora il nostro menu di navigazione presenterà 3 nuove voci, ossia quella relativa alla pagina di registrazione/login, quella relativa al profilo ed il link di logout.

<li class="nav-item">
                    <a class="nav-link" href="{{ route('register') }}">Login / Signup</a>
                </li>

                @if(session('logged_in'))
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('profile') }}">Profile</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('logout') }}">Logout</a>
                    </li>
                @endif

I link al profilo e al logout compariranno solo se il cliente è loggato. Definiamo quindi la logica di logout in un metodo specifico del nostro controller principale.

public function logout(Request $request)
    {
        if($request->session()->exists('logged_in') && $request->session()->exists('customer_id')) {
            $request->session()->forget(['logged_in', 'customer_id']);
        }
        return redirect()->route('home');
    }

In pratica vengono cancellate le due variabili di sessione definite in precedenza nella fase di login.