Digitális aláírás elmentése - Laravel, Vue.js, jSignature
2019 Nov.

Különböző rendezvények helyszíni regisztrációjakor, az adatok megadásának jogszerű elmentéséhez körülményes dupla opt-in megoldást használni, ezért ez felcserélhető digitális aláírással.

Ezt úgy képzeljük el, hogy a helyszínen a hostess elvégzi a gyors regisztrációt, és a végén átnyújtja a tabletet a jelentkezőnek, hogy aláírásával fogadja el a feltételeket.

Erre kiváló megoldás egy jSignature nevű JQuery plugin, amelyet beépítve Vue.js-be és Laravelt mint backendet használva könnyen megvalósítható a feladat.

 

A kiinduló állapot egy telepített és beállított Laravel + Vue rendszer, Laravel 5.5+ lehetőleg. Telepítsük a jSignature node csomagot (https://www.npmjs.com/package/jsignature)

$ npm i --save jsignature

A webpack.mix.js fájlhoz adjuk hozzá a következő sort:

mix.copy('node_modules/jsignature/libs/jSignature.min.js', 'public/vendors/jsignature');

Az aláírást egy Vue komponensben fogjuk használni, amely kódját az app.js-be illesszük be:


Vue.component("j-signature", {
      template: `
    <div class="signature-container">
        <div class="signature"></div>
        <button v-on:click="onClickClear" class="float-right">Törlés</button>
    </div>
    `,
      mounted: function() {
        var self = this;

        var settings = {
          width: '100%',
          height: 160
        };

        this.sigDiv = $(this.$el.children[0]).jSignature(settings);

        this.sigDiv.bind("change", function(event) {
          var data = self.sigDiv.jSignature("getData", "native");

          if (!data.length) {
            self.$emit("change-signature", null);
            return;
          }

          var formattedData = self.sigDiv.jSignature("getData", "default");
          self.$emit("change-signature", formattedData);
        });
      },
      methods: {
        onClickUndo: function() {
          var data = this.sigDiv.jSignature("getData", "native"),
            newData = [];

          if (!data.length) {
            return;
          }

          if (data.length == 1) {
            this.sigDiv.jSignature("reset");
            return;
          }

          for (let index = 0; index < data.length - 1; index++) {
            newData.push(data[index]);
          }

          this.sigDiv.jSignature("setData", newData, "native");
        },
        onClickClear: function() {
          this.sigDiv.jSignature("clear");
        }
      }
    });

A komponens tartalmazza az aláírás html sablonját, amelyben van továbbá egy törlés gomb. Ez azért kell, mert ha elrontja az aláírását (nem is könnyű ujjal aláírni..), akkor ez törölhető legyen (onClickclear() metódus).

A mount() systemhook metódusra inicializálja a jSignature plugint, és ha a mezőbe történik aláírás azaz a plugin saját 'change' eseménye lejátszódik, akkor meg lesz hirdetve egy 'change-signature' Vue esemény.

Ugyanitt, az app.js-ben eltároljuk az elmentett űrlapot:

const regFormLocal = new Vue({
        el: '#regformlocal',
        data: {
            form: {
                company_name : '',
                name : '',
                position : '',
                email : '',
                phone : '',
                gtc : '',
                signature: ''
            },
            allerrors: [],
            success : false
        },
        methods : {
            sendVisitorReg() {
                axios.post('/helyszini-regisztracio', this.form).then( response => {
                    this.allerrors = [];
                    this.form.company_name = '';
                    this.form.name = '';
                    this.form.position = '';
                    this.form.email = '';
                    this.form.phone = '';
                    this.form.gtc = '';
                    this.form.signature = '';
                    this.success = true;
                    $('.signature').jSignature("reset");
                }).catch(error => {
                    if (error.response) {
                        this.success = false;
                        this.allerrors = error.response.data.errors;
                    }
                });
            },
            changeSignature: function(data) {
                this.form.signature = data;
            }
        },
        watch: {
            form: {
                // ha a név változik a formban, a success felirat eltűnik
                handler(newval, oldval) {
                    if (this.form.company_name && this.success) {
                        this.success = false;
                    }
                },
                deep: true
            }
        }
    });

Itt, az űrlap adatain túl tároljuk, hogy egy elküldés sikeres-e vagy nem ('success' boolean változó) és a validációs hibákat tartalmazó tömböt (allerrors).


Két metódus van, a sendVisitorReg() elküldi a backend felé az űrlapot (form változó), majd töröl minden mezőt, az aláírással együtt. A changeSignature() metódus pedig ahogy változás történik az aláírás dobozban, elmenti az aláírás kép adatait base64 kódolással egy rejtett 'signature' input mezőbe.


Egy watcher van, ez pedig az űrlap változásait figyeli, ez arra kell, hogy ha épp látszódik a 'Sikeres regisztráció' felirat, és beírnak újból az űrlapba akkor eltűnjön automatikusan ez a szöveg.


Ez blade view-ba helyezzük el az űrlap kódját:



<section class="regisztracio mt-5" id="regisztracio">
    <div class="container">
    <form action="" accept-charset="utf-8" method="post" id="regformlocal" v-cloak>
        {{ csrf_field() }}
        <div class="row px-lg-5">
            <div class="col-md-4 flex mt-md-4 px-5 px-sm-4 px-md-4 px-lg-0">
                <h3>Helyszíni kiállítás regisztráció</h3>
                <p>A kiállításra a belépés díjmentes, de regisztrációhoz kötött</p>
                <p><img class="plus-svg" src="/images/plus.svg" alt=""></p>
            </div>
            <div class="col-md-4 flex mt-md-4 px-5 px-sm-4 px-md-4 px-lg-0">
                    <div v-if="success" class="success-message">Sikeres regisztráció! Köszönjük!</div>
                    <input autocomplete="off" type="text" id="company_name" name="company_name" v-model="form.company_name" value="" maxlength="255" placeholder="Cégnév *">
                    <div v-if="allerrors.company_name" :class="['label label-danger w-100 ml-5']">@{{ allerrors.company_name[0] }}</div>
                    <input autocomplete="off" type="text" id="name" name="name" value="" maxlength="255" required placeholder="Név *" v-model="form.name">
                    <div v-if="allerrors.name" :class="['label label-danger w-100 ml-5']">@{{ allerrors.name[0] }}</div>
                    <input autocomplete="off" type="text" id="position" name="position" value="" maxlength="255" placeholder="Beosztás *" v-model="form.position">
                    <div v-if="allerrors.position" :class="['label label-danger w-100 ml-5']">@{{ allerrors.position[0] }}</div>
                    <input autocomplete="off" type="text" id="email" name="email" value="" maxlength="255" required placeholder="E-mail *" v-model="form.email">
                    <div v-if="allerrors.email" :class="['label label-danger w-100 ml-5']">@{{ allerrors.email[0] }}</div>
                    <input autocomplete="off" type="text" id="phone" name="phone" value="" maxlength="30" required placeholder="Mobil" v-model="form.phone">
                    <div v-if="allerrors.phone" :class="['label label-danger w-100 ml-5']">@{{ allerrors.phone[0] }}</div>
                    <div class="text-left" style="width:100%; color: #777">Aláírás *</div>
                    <j-signature @change-signature="changeSignature" v-once></j-signature>
                    <input type="hidden" name="signature" id="signature" v-model="form.signature">
                    <div v-if="allerrors.signature" :class="['label label-danger w-100 ml-5']">@{{ allerrors.signature[0] }}</div>

                    <div class="d-flex justify-content-start align-items-center w-100 pl-3 mt-2">
                        <input type="checkbox" name="gtc" style="width:auto" v-model="form.gtc">
                        <span class="ml-1 gtc">Az <a href="" target="_blank" class="one">ÁSZF</a> és az <a href="" class="one" target="_blank">Adatkezelési szabályzat</a> elfogadása</span>
                    </div>
                    <div v-if="allerrors.gtc" :class="['label label-danger w-100 ml-5']">@{{ allerrors.gtc[0] }}</div>
            </div>
            <div class="col-md-4 flex mt-md-4 px-5 px-sm-4 px-md-4 px-lg-0">
                <button type="submit" class="black-button" @click.prevent="sendVisitorReg">Regisztrálok</button>
            </div>
        </div>
        </form>
    </div>
    </section>

A regisztráció elmentésének metódusa Laravel oldalon:

public function saveLocalRegistration(RegisterLocalVisitor $request, Visitor $visitor)
{
    $visitor = new $visitor;

    $visitor->name = request()->get('name');
    $visitor->company_name = request()->get('company_name');
    $visitor->position = request()->get('position');
    $visitor->email = request()->get('email');
    $visitor->phone = request()->get('phone');
    $visitor->year_id = currentYearId();
    $visitor->signature = request()->get('signature');
    $visitor->attended = 1;

    $visitor->save();

    return response()->json(true);
}

A validáció egy FormRequest osztályban történik:

class RegisterLocalVisitor 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 [
            'company_name' => 'required|max:255',
            'name'         => 'required|max:100',
            'position'     => 'required|max:100',
            'email'        => 'required|email|max:100',
            'gtc'          => 'required|in:0,1',
            'signature'    => 'required'
        ];
    }

    public function messages()
    {
        return [
            'company_name.max'      => 'Maximum :max karakter adható meg!',
            'company_name.required' => 'A Cégnév megadása kötelező!',
            'name.required'         => 'A Név megadása kötelező!',
            'name.max'              => 'A Név maximum :max karakter hosszú lehet!',
            'position.max'          => 'A Beosztás maximum :max karakter hosszú lehet!',
            'position.required'     => 'Az Beosztás megadása kötelező!',
            'email.required'        => 'Az E-mail megadása kötelező!',
            'email.email'           => 'Az E-mail helytelen formátumú!',
            'email.max'             => 'Az E-mail maximum :max karakter hosszú lehet!',
            'gtc.required'          => 'Az ÁSZF és Adatkezelési szabályzat elfogadása kötelező!',
            'gtc.in'                => 'Hibás adat megadva az ÁSZF és Adatkezelési szabályzatnál!',
            'signature.required'    => 'Az Aláírás megadása kötelező!'
        ];
    }

    public function response(array $errors)
    {
        if ($this->expectsJson()) {
            return new JsonResponse($errors, 200);
        }
    }
}

Itt egy fontos módosítás kell, a response() metódust felül kell írni, hogy az ErrorBag visszaérkezzen az axios híváshoz.