feat(auth): integrate codice-fiscale-js for fiscal code validation in registration
This commit is contained in:
+183
-109
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user