feat(auth): integrate codice-fiscale-js for fiscal code validation in registration

This commit is contained in:
2026-06-14 19:54:18 +02:00 Unverified
parent 762a42196c
commit 038a399e93
7 changed files with 895 additions and 386 deletions
+183 -109
View File
@@ -54,131 +54,205 @@ const handleSubmit = async () => {
</script>
<template>
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-12 col-md-8 col-lg-6">
<div v-if="user?.must_change_password" class="callout callout-highlight callout-warning mb-4">
<div class="callout-title d-flex align-items-center">
<BIIcon name="it-warning-circle" size="sm" color="warning" class="me-2" />
Azione Richiesta
</div>
<p>Per motivi di sicurezza, imposta una nuova password per il tuo account.</p>
</div>
<div class="card card-bg shadow-sm">
<div class="card-body p-4">
<h3 class="card-title h4 mb-4">Cambia la tua password</h3>
<form @submit.prevent="handleSubmit">
<div class="form-group mb-3">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text"><BIIcon name="it-lock" size="sm" /></div>
<div class="password-reset-page py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<div class="row g-4 align-items-stretch">
<div class="col-12 col-lg-5">
<section class="password-reset-intro h-100">
<span class="password-reset-eyebrow">Accesso protetto</span>
<h1 class="h2 mb-3">Imposta nuova password</h1>
<p class="mb-4">
Aggiorna credenziali prima di continuare. Password nuova deve essere diversa da quella attuale.
</p>
<div v-if="user?.must_change_password" class="callout callout-highlight callout-warning mb-0">
<div class="callout-title d-flex align-items-center">
<BIIcon name="it-warning-circle" size="sm" color="warning" class="me-2" />
Azione richiesta
</div>
<input
:type="showCurrent ? 'text' : 'password'"
class="form-control"
id="current_password"
v-model="formData.current_password"
>
<!-- FIX: Aggiunta classe 'active' dinamica per evitare sovrapposizione placeholder -->
<label for="current_password" :class="{ 'active': formData.current_password }">Password attuale</label>
<div class="input-group-append">
<button type="button" class="btn btn-primary btn-sm" @click="showCurrent = !showCurrent">
<BIIcon :name="showCurrent ? 'it-password-visible' : 'it-password-invisible'" size="sm" color="white" />
<p class="mb-0">Per motivi di sicurezza, imposta una nuova password per il tuo account.</p>
</div>
</section>
</div>
<div class="col-12 col-lg-7">
<div class="card card-bg shadow-sm border-0 h-100">
<div class="card-body p-4 p-lg-5">
<h2 class="card-title h4 mb-4">Cambia password</h2>
<form @submit.prevent="handleSubmit" class="d-grid gap-4">
<div class="password-field">
<label for="current_password" class="form-label">Password attuale</label>
<div class="password-field-control">
<BIIcon name="it-lock" size="sm" class="password-field-icon" />
<input
id="current_password"
v-model="formData.current_password"
:type="showCurrent ? 'text' : 'password'"
class="form-control"
autocomplete="current-password"
>
<button
type="button"
class="btn btn-password-toggle"
:aria-label="showCurrent ? 'Nascondi password attuale' : 'Mostra password attuale'"
@click="showCurrent = !showCurrent"
>
<BIIcon :name="showCurrent ? 'it-password-visible' : 'it-password-invisible'" size="sm" />
</button>
</div>
</div>
<div class="password-field">
<label for="new_password" class="form-label">Nuova password</label>
<div class="password-field-control">
<BIIcon name="it-lock" size="sm" class="password-field-icon" />
<input
id="new_password"
v-model="formData.new_password"
:type="showNew ? 'text' : 'password'"
class="form-control"
autocomplete="new-password"
>
<button
type="button"
class="btn btn-password-toggle"
:aria-label="showNew ? 'Nascondi nuova password' : 'Mostra nuova password'"
@click="showNew = !showNew"
>
<BIIcon :name="showNew ? 'it-password-visible' : 'it-password-invisible'" size="sm" />
</button>
</div>
<div class="form-text">Minimo 8 caratteri.</div>
</div>
<div class="password-field">
<label for="confirm_password" class="form-label">Conferma nuova password</label>
<div class="password-field-control">
<BIIcon name="it-lock" size="sm" class="password-field-icon" />
<input
id="confirm_password"
v-model="formData.confirm_password"
:type="showConfirm ? 'text' : 'password'"
:class="{ 'is-invalid': formData.confirm_password && formData.confirm_password !== formData.new_password }"
class="form-control"
autocomplete="new-password"
>
<button
type="button"
class="btn btn-password-toggle"
:aria-label="showConfirm ? 'Nascondi conferma password' : 'Mostra conferma password'"
@click="showConfirm = !showConfirm"
>
<BIIcon :name="showConfirm ? 'it-password-visible' : 'it-password-invisible'" size="sm" />
</button>
</div>
</div>
<div v-if="errorMessage" class="alert alert-danger mb-0" role="alert">
{{ errorMessage }}
</div>
<button
type="submit"
class="btn btn-primary w-100 py-2"
:disabled="!canSubmit || isLoading"
>
<span v-if="isLoading" class="spinner-border spinner-border-sm me-2"></span>
{{ isLoading ? 'Aggiornamento in corso' : 'Aggiorna password' }}
</button>
</div>
</form>
</div>
</div>
<hr class="my-4">
<!-- 2. NUOVA PASSWORD -->
<div class="form-group mb-3">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text"><BIIcon name="it-lock" size="sm" /></div>
</div>
<input
:type="showNew ? 'text' : 'password'"
class="form-control"
id="new_password"
v-model="formData.new_password"
>
<label for="new_password" :class="{ 'active': formData.new_password }">Nuova password</label>
<div class="input-group-append">
<button type="button" class="btn btn-primary btn-sm" @click="showNew = !showNew">
<BIIcon :name="showNew ? 'it-password-visible' : 'it-password-invisible'" size="sm" color="white" />
</button>
</div>
</div>
<small class="form-text text-muted">Minimo 8 caratteri.</small>
</div>
<!-- 3. CONFERMA PASSWORD -->
<div class="form-group mb-4">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text"><BIIcon name="it-lock" size="sm" /></div>
</div>
<input
:type="showConfirm ? 'text' : 'password'"
class="form-control"
:class="{ 'is-invalid': formData.confirm_password && formData.confirm_password !== formData.new_password }"
id="confirm_password"
v-model="formData.confirm_password"
>
<label for="confirm_password" :class="{ 'active': formData.confirm_password }">Conferma nuova password</label>
<div class="input-group-append">
<button type="button" class="btn btn-primary btn-sm" @click="showConfirm = !showConfirm">
<BIIcon :name="showConfirm ? 'it-password-visible' : 'it-password-invisible'" size="sm" color="white" />
</button>
</div>
</div>
</div>
<div v-if="errorMessage" class="alert alert-danger mb-4" role="alert">
{{ errorMessage }}
</div>
<button
type="submit"
class="btn btn-primary w-100 py-2"
:disabled="!canSubmit || isLoading"
>
<span v-if="isLoading" class="spinner-border spinner-border-sm me-2"></span>
<span v-else>Aggiorna Password</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* Correzione per i bottoni occhio in Bootstrap Italia */
.input-group-append .btn {
z-index: 4;
.password-reset-page {
background: linear-gradient(180deg, #f0f6fc 0%, #ffffff 65%);
}
.password-reset-intro {
background: #17324d;
border-radius: 8px;
color: #fff;
padding: 2rem;
}
.password-reset-intro :deep(h1),
.password-reset-intro :deep(p),
.password-reset-intro :deep(span) {
color: inherit;
}
.password-reset-eyebrow {
display: inline-block;
font-size: 0.8125rem;
font-weight: 700;
letter-spacing: 0.04em;
margin-bottom: 1rem;
text-transform: uppercase;
}
.password-field {
display: grid;
gap: 0.5rem;
}
.password-field-control {
align-items: center;
background: #fff;
border: 1px solid #c5d0db;
border-radius: 8px;
display: grid;
gap: 0.5rem;
grid-template-columns: auto minmax(0, 1fr) auto;
padding-left: 0.875rem;
}
.password-field-control:focus-within {
border-color: #06c;
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.16);
}
.password-field-icon {
color: #5c6f82;
}
.password-field-control .form-control {
background: transparent;
border: 0;
border-radius: 0;
box-shadow: none;
min-width: 0;
padding-left: 0;
padding-right: 0;
}
.btn-password-toggle {
align-items: center;
color: #06c;
display: inline-flex;
height: 100%;
justify-content: center;
min-height: 3rem;
padding: 0 0.875rem;
}
/* Stile per gestire correttamente le label di Bootstrap Italia che si sovrappongono */
.form-group label.active {
transform: translateY(-140%) scale(0.9);
color: #006f9d;
.btn-password-toggle:hover,
.btn-password-toggle:focus {
background: rgba(0, 102, 204, 0.08);
color: #06c;
}
.input-group-text {
background-color: #f0f0f0;
border-right: none;
}
.form-control:focus + label {
color: #006f9d;
@media (max-width: 991.98px) {
.password-reset-intro {
padding: 1.5rem;
}
}
</style>
+15 -25
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import CodiceFiscale from 'codice-fiscale-js'
import { ref } from 'vue'
definePageMeta({
@@ -25,22 +26,7 @@ const form = ref({
privacy: false
})
const computeSurnameCode = (surname = '') => {
const s = surname.toUpperCase().replace(/[^A-Z]/g, '')
const consonants = s.replace(/[AEIOU]/g, '')
const vowels = s.replace(/[^AEIOU]/g, '')
return (consonants + vowels + 'XXX').slice(0, 3)
}
const computeNameCode = (name = '') => {
const s = name.toUpperCase().replace(/[^A-Z]/g, '')
const consonants = s.replace(/[AEIOU]/g, '')
const vowels = s.replace(/[^AEIOU]/g, '')
if (consonants.length >= 4) {
return consonants.charAt(0) + consonants.charAt(2) + consonants.charAt(3)
}
return (consonants + vowels + 'XXX').slice(0, 3)
}
const normalizeFiscalCode = (fiscalCode = '') => fiscalCode.trim().toUpperCase()
const handleRegister = async () => {
if (!form.value.privacy) {
@@ -48,13 +34,17 @@ const handleRegister = async () => {
return
}
const expected = computeSurnameCode(form.value.last_name) + computeNameCode(form.value.first_name)
if (form.value.fiscal_code.trim().length >= 6) {
const provided = form.value.fiscal_code.trim().toUpperCase().slice(0, 6)
if (provided !== expected) {
errorMessage.value = `Il Codice Fiscale non sembra corrispondere a nome/cognome. Atteso: ${expected}`
return
}
const fiscalCode = normalizeFiscalCode(form.value.fiscal_code)
const expected = CodiceFiscale.surnameCode(form.value.last_name) + CodiceFiscale.nameCode(form.value.first_name)
if (!CodiceFiscale.check(fiscalCode)) {
errorMessage.value = 'Il Codice Fiscale inserito non è valido.'
return
}
if (fiscalCode.slice(0, 6) !== expected) {
errorMessage.value = 'Il Codice Fiscale non corrisponde a nome e cognome inseriti.'
return
}
isLoading.value = true
@@ -64,7 +54,7 @@ const handleRegister = async () => {
await register({
first_name: form.value.first_name,
last_name: form.value.last_name,
fiscal_code: form.value.fiscal_code.toUpperCase(),
fiscal_code: fiscalCode,
email: form.value.email,
password: form.value.password,
phone: form.value.phone || undefined
@@ -199,4 +189,4 @@ const handleRegister = async () => {
.pe-5 {
padding-right: 3rem !important;
}
</style>
</style>