refactor: improve auth login and register pages with UX improvements

- Add guest middleware to login page
- Integrate auth handles for handleLoginSuccess on verifyOtp and passwordLogin
- Add password visibility toggle to register and login password inputs
- Calculate and validate Italian Fiscal Code structure against name/surname input on register
- Align form-group structure within row cols for Bootstrap Italia layout compliance
This commit is contained in:
2026-06-12 19:04:19 +02:00 Unverified
parent 7499ca60f6
commit cfb5d629c7
2 changed files with 111 additions and 43 deletions
+32 -22
View File
@@ -1,9 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
definePageMeta({
middleware: 'guest'
})
useHead({ title: 'Accedi - Visione' }) useHead({ title: 'Accedi - Visione' })
const { requestOtp, verifyOtp, loginWithPassword } = useAuth() const { requestOtp, verifyOtp, loginWithPassword, handleLoginSuccess } = useAuth()
const router = useRouter() const router = useRouter()
// UI State // UI State
@@ -16,8 +20,8 @@ const errorMessage = ref('')
const email = ref('') const email = ref('')
const password = ref('') const password = ref('')
const otpCode = ref('') const otpCode = ref('')
const showPassword = ref(false)
// Azione 1: Richiedi OTP
const handleRequestOtp = async () => { const handleRequestOtp = async () => {
isLoading.value = true isLoading.value = true
errorMessage.value = '' errorMessage.value = ''
@@ -31,16 +35,13 @@ const handleRequestOtp = async () => {
} }
} }
// Azione 2: Verifica OTP
const handleVerifyOtp = async () => { const handleVerifyOtp = async () => {
isLoading.value = true isLoading.value = true
errorMessage.value = '' errorMessage.value = ''
try { try {
const res = await verifyOtp({ email: email.value, code: otpCode.value }) const res = await verifyOtp({ email: email.value, code: otpCode.value })
// Salva il token in un cookie (gestito automaticamente da Nuxt e accessibile in useApiFetch) await handleLoginSuccess(res.token)
const tokenCookie = useCookie('auth_token', { maxAge: res.expires_in_hours * 3600 }) router.push('/dashboard')
tokenCookie.value = res.token
router.push('/dashboard') // O qualsiasi pagina post-login
} catch (err: any) { } catch (err: any) {
errorMessage.value = 'Codice OTP non valido o scaduto.' errorMessage.value = 'Codice OTP non valido o scaduto.'
} finally { } finally {
@@ -48,14 +49,12 @@ const handleVerifyOtp = async () => {
} }
} }
// Azione 3: Login con Password (Fallback)
const handlePasswordLogin = async () => { const handlePasswordLogin = async () => {
isLoading.value = true isLoading.value = true
errorMessage.value = '' errorMessage.value = ''
try { try {
const res = await loginWithPassword({ email: email.value, password: password.value }) const res = await loginWithPassword({ email: email.value, password: password.value })
const tokenCookie = useCookie('auth_token', { maxAge: res.expires_in_hours * 3600 }) await handleLoginSuccess(res.token)
tokenCookie.value = res.token
router.push('/dashboard') router.push('/dashboard')
} catch (err: any) { } catch (err: any) {
errorMessage.value = 'Credenziali non valide.' errorMessage.value = 'Credenziali non valide.'
@@ -66,7 +65,7 @@ const handlePasswordLogin = async () => {
</script> </script>
<template> <template>
<div class="d-flex align-items-center min-vh-100 bg-light py-5"> <div class="d-flex align-items-center bg-light py-5">
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-12 col-md-8 col-lg-5"> <div class="col-12 col-md-8 col-lg-5">
@@ -81,7 +80,7 @@ const handlePasswordLogin = async () => {
<div class="card card-bg shadow-sm"> <div class="card card-bg shadow-sm">
<div class="card-body p-4 p-md-5"> <div class="card-body p-4 p-md-5">
<!-- Finto Bottone CIE (Stile Istituzionale) --> <!-- Finto Bottone CIE -->
<button class="btn btn-primary w-100 mb-4 d-flex align-items-center justify-content-center gap-2" disabled title="Integrazione CIE prevista per la fase successiva"> <button class="btn btn-primary w-100 mb-4 d-flex align-items-center justify-content-center gap-2" disabled title="Integrazione CIE prevista per la fase successiva">
<BIIcon name="it-card" size="sm" color="white" /> <BIIcon name="it-card" size="sm" color="white" />
Entra con CIE (Presto disponibile) Entra con CIE (Presto disponibile)
@@ -89,23 +88,19 @@ const handlePasswordLogin = async () => {
<div class="d-flex align-items-center my-4"> <div class="d-flex align-items-center my-4">
<hr class="flex-grow-1"> <hr class="flex-grow-1">
<span class="mx-3 text-muted small text-uppercase">Oppure accedi con Email</span> <span class="mx-3 text-muted small text-uppercase">Oppure</span>
<hr class="flex-grow-1"> <hr class="flex-grow-1">
</div> </div>
<!-- Messaggio di errore -->
<div v-if="errorMessage" class="alert alert-danger mb-4" role="alert"> <div v-if="errorMessage" class="alert alert-danger mb-4" role="alert">
{{ errorMessage }} {{ errorMessage }}
</div> </div>
<!-- FLUSSO OTP --> <!-- FLUSSO OTP -->
<div v-if="loginMethod === 'otp'"> <div v-if="loginMethod === 'otp'">
<!-- Step 1: Inserisci Email -->
<form v-if="otpStep === 1" @submit.prevent="handleRequestOtp"> <form v-if="otpStep === 1" @submit.prevent="handleRequestOtp">
<div class="form-group mb-4"> <div class="form-group mb-4">
<input type="email" class="form-control" id="email" v-model="email" required> <input type="email" class="form-control" id="email" v-model="email" required>
<!-- Label flottante di Bootstrap Italia (attiva se c'è testo) -->
<label for="email" :class="{ 'active': email.length > 0 }">Indirizzo Email</label> <label for="email" :class="{ 'active': email.length > 0 }">Indirizzo Email</label>
</div> </div>
<button type="submit" class="btn btn-primary w-100" :disabled="isLoading"> <button type="submit" class="btn btn-primary w-100" :disabled="isLoading">
@@ -114,7 +109,6 @@ const handlePasswordLogin = async () => {
</button> </button>
</form> </form>
<!-- Step 2: Inserisci Codice -->
<form v-else @submit.prevent="handleVerifyOtp"> <form v-else @submit.prevent="handleVerifyOtp">
<div class="alert alert-success mb-4 small"> <div class="alert alert-success mb-4 small">
Abbiamo inviato un codice OTP a <strong>{{ email }}</strong>. Abbiamo inviato un codice OTP a <strong>{{ email }}</strong>.
@@ -131,19 +125,32 @@ const handlePasswordLogin = async () => {
<a href="#" class="small text-decoration-none" @click.prevent="otpStep = 1">Modifica indirizzo email</a> <a href="#" class="small text-decoration-none" @click.prevent="otpStep = 1">Modifica indirizzo email</a>
</div> </div>
</form> </form>
</div> </div>
<!-- FLUSSO PASSWORD (Fallback) --> <!-- FLUSSO PASSWORD -->
<form v-else @submit.prevent="handlePasswordLogin"> <form v-else @submit.prevent="handlePasswordLogin">
<div class="form-group mb-4"> <div class="form-group mb-4">
<input type="email" class="form-control" id="emailPwd" v-model="email" required> <input type="email" class="form-control" id="emailPwd" v-model="email" required>
<label for="emailPwd" :class="{ 'active': email.length > 0 }">Indirizzo Email</label> <label for="emailPwd" :class="{ 'active': email.length > 0 }">Indirizzo Email</label>
</div> </div>
<div class="form-group mb-4">
<input type="password" class="form-control" id="password" v-model="password" required> <!-- INPUT PASSWORD FIXATO -->
<div class="form-group mb-4 position-relative">
<input :type="showPassword ? 'text' : 'password'" class="form-control pe-5" id="password" v-model="password" required>
<label for="password" :class="{ 'active': password.length > 0 }">Password</label> <label for="password" :class="{ 'active': password.length > 0 }">Password</label>
<button
type="button"
class="btn bg-transparent border-0 shadow-none position-absolute p-2"
style="right: 0; top: 2px; z-index: 10;"
@click="showPassword = !showPassword"
:aria-pressed="showPassword"
aria-label="Mostra/Nascondi password"
>
<BIIcon :name="showPassword ? 'it-password-visible' : 'it-password-invisible'" color="primary" size="sm" />
</button>
</div> </div>
<button type="submit" class="btn btn-primary w-100 mb-3" :disabled="isLoading"> <button type="submit" class="btn btn-primary w-100 mb-3" :disabled="isLoading">
<span v-if="isLoading" class="spinner-border spinner-border-sm me-2" role="status"></span> <span v-if="isLoading" class="spinner-border spinner-border-sm me-2" role="status"></span>
Accedi Accedi
@@ -174,4 +181,7 @@ const handlePasswordLogin = async () => {
.text-tracking-widest { .text-tracking-widest {
letter-spacing: 0.5em; letter-spacing: 0.5em;
} }
.pe-5 {
padding-right: 3rem !important;
}
</style> </style>
+78 -20
View File
@@ -1,6 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
definePageMeta({
middleware: 'guest'
})
useHead({ title: 'Registrazione - Visione' }) useHead({ title: 'Registrazione - Visione' })
const { register } = useAuth() const { register } = useAuth()
@@ -9,8 +13,8 @@ const router = useRouter()
const isLoading = ref(false) const isLoading = ref(false)
const errorMessage = ref('') const errorMessage = ref('')
const successMessage = ref(false) const successMessage = ref(false)
const showPassword = ref(false)
// Form State mappato sull'API RegisterCitizenRequest
const form = ref({ const form = ref({
first_name: '', first_name: '',
last_name: '', last_name: '',
@@ -21,12 +25,38 @@ const form = ref({
privacy: false 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 handleRegister = async () => { const handleRegister = async () => {
if (!form.value.privacy) { if (!form.value.privacy) {
errorMessage.value = "È necessario accettare l'informativa sulla privacy." errorMessage.value = "È necessario accettare l'informativa sulla privacy."
return 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
}
}
isLoading.value = true isLoading.value = true
errorMessage.value = '' errorMessage.value = ''
@@ -41,7 +71,6 @@ const handleRegister = async () => {
}) })
successMessage.value = true successMessage.value = true
// Ritarda il redirect per far leggere il messaggio
setTimeout(() => { setTimeout(() => {
router.push('/auth/login') router.push('/auth/login')
}, 3000) }, 3000)
@@ -55,7 +84,7 @@ const handleRegister = async () => {
</script> </script>
<template> <template>
<div class="d-flex align-items-center min-vh-100 bg-light py-5"> <div class="d-flex align-items-center bg-light py-5">
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-12 col-lg-8"> <div class="col-12 col-lg-8">
@@ -70,7 +99,6 @@ const handleRegister = async () => {
<div class="card-body p-4 p-md-5"> <div class="card-body p-4 p-md-5">
<div v-if="successMessage" class="alert alert-success" role="alert"> <div v-if="successMessage" class="alert alert-success" role="alert">
<BIIcon name="it-check-circle" size="sm" class="me-2" />
Registrazione completata con successo! Reindirizzamento al login... Registrazione completata con successo! Reindirizzamento al login...
</div> </div>
@@ -81,14 +109,20 @@ const handleRegister = async () => {
<form v-if="!successMessage" @submit.prevent="handleRegister"> <form v-if="!successMessage" @submit.prevent="handleRegister">
<h2 class="h5 text-primary font-weight-bold mb-4 border-bottom pb-2">Dati Anagrafici</h2> <h2 class="h5 text-primary font-weight-bold mb-4 border-bottom pb-2">Dati Anagrafici</h2>
<div class="row">
<div class="col-12 col-md-6 form-group mb-4"> <!-- FIX LAYOUT: form-group separato da col -->
<input type="text" class="form-control" id="nome" v-model="form.first_name" required> <div class="row mb-4">
<label for="nome" :class="{ 'active': form.first_name.length > 0 }">Nome</label> <div class="col-12 col-md-6 mb-4 mb-md-0">
<div class="form-group">
<input type="text" class="form-control" id="nome" v-model="form.first_name" required>
<label for="nome" :class="{ 'active': form.first_name.length > 0 }">Nome</label>
</div>
</div> </div>
<div class="col-12 col-md-6 form-group mb-4"> <div class="col-12 col-md-6">
<input type="text" class="form-control" id="cognome" v-model="form.last_name" required> <div class="form-group">
<label for="cognome" :class="{ 'active': form.last_name.length > 0 }">Cognome</label> <input type="text" class="form-control" id="cognome" v-model="form.last_name" required>
<label for="cognome" :class="{ 'active': form.last_name.length > 0 }">Cognome</label>
</div>
</div> </div>
</div> </div>
@@ -99,20 +133,37 @@ const handleRegister = async () => {
</div> </div>
<h2 class="h5 text-primary font-weight-bold mt-5 mb-4 border-bottom pb-2">Credenziali e Contatti</h2> <h2 class="h5 text-primary font-weight-bold mt-5 mb-4 border-bottom pb-2">Credenziali e Contatti</h2>
<div class="row">
<div class="col-12 col-md-6 form-group mb-4"> <div class="row mb-4">
<input type="email" class="form-control" id="regEmail" v-model="form.email" required> <div class="col-12 col-md-6 mb-4 mb-md-0">
<label for="regEmail" :class="{ 'active': form.email.length > 0 }">Indirizzo Email</label> <div class="form-group">
<input type="email" class="form-control" id="regEmail" v-model="form.email" required>
<label for="regEmail" :class="{ 'active': form.email.length > 0 }">Indirizzo Email</label>
</div>
</div> </div>
<div class="col-12 col-md-6 form-group mb-4"> <div class="col-12 col-md-6">
<input type="password" class="form-control" id="regPwd" v-model="form.password" required minlength="10"> <!-- INPUT PASSWORD FIXATO -->
<label for="regPwd" :class="{ 'active': form.password.length > 0 }">Password (min. 10 crt)</label> <div class="form-group position-relative">
<input :type="showPassword ? 'text' : 'password'" class="form-control pe-5" id="regPwd" v-model="form.password" required minlength="10">
<label for="regPwd" :class="{ 'active': form.password.length > 0 }">Password (min. 10 crt)</label>
<button
type="button"
class="btn bg-transparent border-0 shadow-none position-absolute p-2"
style="right: 0; top: 2px; z-index: 10;"
@click="showPassword = !showPassword"
:aria-pressed="showPassword"
aria-label="Mostra/Nascondi password"
>
<BIIcon :name="showPassword ? 'it-password-visible' : 'it-password-invisible'" color="primary" size="sm" />
</button>
</div>
</div> </div>
</div> </div>
<div class="form-group mb-5"> <div class="form-group mb-5">
<input type="tel" class="form-control" id="phone" v-model="form.phone"> <input type="tel" class="form-control" id="phone" v-model="form.phone">
<label for="phone" :class="{ 'active': form.phone.length > 0 }">Numero di telefono (Opzionale)</label> <label for="phone" :class="{ 'active': form.phone?.length > 0 }">Numero di telefono (Opzionale)</label>
<small class="form-text text-muted">Faciliterà il contatto da parte degli operatori comunali in caso di intervento.</small> <small class="form-text text-muted">Faciliterà il contatto da parte degli operatori comunali in caso di intervento.</small>
</div> </div>
@@ -120,7 +171,7 @@ const handleRegister = async () => {
<div class="form-check mb-4"> <div class="form-check mb-4">
<input class="form-check-input" type="checkbox" id="privacyCheck" v-model="form.privacy" required> <input class="form-check-input" type="checkbox" id="privacyCheck" v-model="form.privacy" required>
<label class="form-check-label small" for="privacyCheck"> <label class="form-check-label small" for="privacyCheck">
Dichiaro di aver letto l'<NuxtLink to="/privacy" target="_blank">Informativa sulla Privacy</NuxtLink> e accetto il trattamento dei dati personali ai fini dell'erogazione del servizio. Dichiaro di aver letto l'<NuxtLink to="/privacy" target="_blank">Informativa sulla Privacy</NuxtLink> e accetto il trattamento dei dati personali.
</label> </label>
</div> </div>
@@ -142,3 +193,10 @@ const handleRegister = async () => {
</div> </div>
</div> </div>
</template> </template>
<style scoped>
/* Previene che il testo finisca sotto l'icona */
.pe-5 {
padding-right: 3rem !important;
}
</style>