Creare un componente Vue.js per gestire i codici della 2FA

Solitamente i codici per l'autenticazione a due fattori (2FA) vengono inseriti su più di un campo di input, uno per cifra. Questa è una possibile scelta di design che ci spinge ad implementare una soluzione frontend che garantisca sia il layout finale scelto sia la validità del codice inserito. In questo articolo, vedremo come creare un componente Vue.js dedicato a questo scopo.

Requisiti del componente

Il nostro componente dovrà:

  1. Generare un numero arbitrario n di campi di input.
  2. Permettere a ciascun campo di input di ricevere l'evento focus non appena il campo precedente è stato modificato.
  3. Emettere un evento che invierà al componente genitore il codice come stringa unica quando tutti i campi sono stati modificati.

Per implementare correttamente queste funzionalità, sarà inoltre necessario intercettare gli eventi input (l'utente digita la cifra nel campo) e paste (l'utente incolla dagli appunti la cifra nel campo) dato che il nostro componente avrà unicamente i campi di input, senza un pulsante.

Infine, il nostro componente accetterà come prop un numero intero (length) che corrisponderà al numero di cifre del codice e quindi al numero di campi di input da generare.

Struttura iniziale

All'inizio andremo a definire la prop, l'evento del componente e un array vuoto che sarà popolato con i dati dei campi. Inseriamo anche l'evento onMounted che ci servirà in seguito.

<script setup>
import { ref, onMounted } from 'vue';

const props = defineProps({
    length: Number
});

const emits = defineEmits(['select']);

const fields = ref([]);

onMounted(() => {
    // Popoliamo fields
});
</script>

<template>
    <div class="twofa-code-input">
        <!-- Loop sui fields -->
    </div>
</template>

Generare i dati dei campi

Volendo sfruttare il binding di Vue.js con v-model, dobbiamo pensare all'array fields come ad un array di oggetti aventi ciascuno due proprietà:

  1. value: Il valore inserito dall'utente nel campo.
  2. index: L'indice (zero-based) del campo nell'array fields, che ci servirà per sapere se l'utente ha modificato l'ultimo campo.

Quindi scriveremo:

const create = (len = 6) => {
    const data = [];
    for(let i = 0; i < len; i++) {
        data.push({
            value: '',
            index: i
        });
    }
    return data;
};

onMounted(() => {
    fields.value = create(props.length);
});

Usando il valore del numero contenuto in length andiamo a valorizzare i campi all'avvenuto mount del nostro componente.

Gestire l'inserimento dell'input

A questo punto dobbiamo gestire il focus automatico sul campo successivo e verificare che anche l'ultimo campo sia stato modificato per poter inviare l'evento select che avrà come dato l'intero codice inserito.

L'oggetto evento passato come argomento ai listener input e change contiene la proprietà target che è un riferimento DOM al campo corrente. Partendo da questo riferimento, possiamo leggere il valore del campo tramite la sua proprietà value e:

  1. Individuare l'oggetto corrispondente nell'array fields tramite la proprietà value.
  2. Selezionare (se esiste) nel DOM il campo successivo ed innescare l'evento focus.
  3. Verificare che siamo sull'ultimo campo usando il valore della proprietà index dell'oggetto corrente e confrontarla con il valore di props.length - 1.
const handleChange = (evt) => {
    const input = evt.target;
    const value = input.value;
    if(/^\d$/.test(value)) {
        const field = fields.value.find(f => f.value === value);
        const next = input.parentNode.nextElementSibling;
        if(next) {
            next.querySelector('input').focus();
        }
        if(field) {
            let cur = props.length - 1;
            if(parseInt(field.index, 10) === parseInt(cur, 10)) {
                emits('select', fields.value.map(f => f.value).join(''));
            }
        }
    }
};

Ora il nostro template apparirà così:

<template>
    <div class="twofa-code-input">
        <div class="twofa-code-input-wrap" v-for="field in fields" :key="field.index">
            <input @paste="handleChange" @input="handleChange" type="text" v-model="field.value" />
        </div>
    </div>
</template>

Uso del componente

Per usare il nostro componente occorre semplicemente definire un handler per l'evento select.

<script setup>
import TwoFactorCode from '@/components/TwoFactorCode.vue';

const handleSelectOtpCode = code => {
    console.log(code);
};
</script>

<template>
    <TwoFactorCode @select="handleSelectOtpCode" :length="6" />
</template>

Miglioramenti

Agnosticismo sul formato

Attualmente gestiamo solo cifre ma in futuro potremmo voler gestire anche caratteri alfabetici o una combinazione dei due.

Di conseguenza la nostra espressione regolare che usiamo per validare i valori di input dovrà adattarsi di conseguenza.

Incollare l'intero codice

L'utente potrebbe voler incollare l'intero codice di n-cifre non appena seleziona il primo campo.

Per implementare questa funzionalità dovremo:

  1. Verificare che la lunghezza del codice incollato sia pari a length.
  2. Trasformare la stringa ricevuta in un array.
  3. Ripopolare l'array fields con i nuovi dati.
  4. Emettere l'evento select con il codice appena inserito.

Conclusione

Grazie alla modularità di Vue.js è relativamente semplice implementare un genere di componenti molto usato nei form di autenticazione a due passaggi.