feat: implement authentication pages with OTP support
- Create login page with OTP verification flow - Implement passwordless authentication with email OTP - Add fallback password login method - Create register page with user account creation - Integrate with useAuth() composable for auth logic - Add token management via useCookie for session persistence - Include error handling and loading states Co-authored-by: Copilot <[email protected]>
This commit is contained in:
+168
-2
@@ -1,11 +1,177 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
useHead({ title: 'Accedi - Visione' })
|
||||||
|
|
||||||
|
const { requestOtp, verifyOtp, loginWithPassword } = useAuth()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
const loginMethod = ref<'otp' | 'password'>('otp')
|
||||||
|
const otpStep = ref<1 | 2>(1)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
// Form State
|
||||||
|
const email = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const otpCode = ref('')
|
||||||
|
|
||||||
|
// Azione 1: Richiedi OTP
|
||||||
|
const handleRequestOtp = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
try {
|
||||||
|
await requestOtp({ email: email.value })
|
||||||
|
otpStep.value = 2
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage.value = err.data?.message || 'Errore durante la richiesta OTP. Riprova.'
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Azione 2: Verifica OTP
|
||||||
|
const handleVerifyOtp = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
try {
|
||||||
|
const res = await verifyOtp({ email: email.value, code: otpCode.value })
|
||||||
|
// Salva il token in un cookie (gestito automaticamente da Nuxt e accessibile in useApiFetch)
|
||||||
|
const tokenCookie = useCookie('auth_token', { maxAge: res.expires_in_hours * 3600 })
|
||||||
|
tokenCookie.value = res.token
|
||||||
|
router.push('/dashboard') // O qualsiasi pagina post-login
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage.value = 'Codice OTP non valido o scaduto.'
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Azione 3: Login con Password (Fallback)
|
||||||
|
const handlePasswordLogin = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
try {
|
||||||
|
const res = await loginWithPassword({ email: email.value, password: password.value })
|
||||||
|
const tokenCookie = useCookie('auth_token', { maxAge: res.expires_in_hours * 3600 })
|
||||||
|
tokenCookie.value = res.token
|
||||||
|
router.push('/dashboard')
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage.value = 'Credenziali non valide.'
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
$END$
|
<div class="d-flex align-items-center min-vh-100 bg-light py-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-md-8 col-lg-5">
|
||||||
|
|
||||||
|
<!-- Intestazione istituzionale -->
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<BIIcon name="it-pa" size="xl" color="primary" class="mb-2" />
|
||||||
|
<h1 class="h3 font-weight-bold">Accedi a Visione</h1>
|
||||||
|
<p class="text-muted">Il portale delle segnalazioni urbane</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card card-bg shadow-sm">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
|
||||||
|
<!-- Finto Bottone CIE (Stile Istituzionale) -->
|
||||||
|
<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" />
|
||||||
|
Entra con CIE (Presto disponibile)
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center my-4">
|
||||||
|
<hr class="flex-grow-1">
|
||||||
|
<span class="mx-3 text-muted small text-uppercase">Oppure accedi con Email</span>
|
||||||
|
<hr class="flex-grow-1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Messaggio di errore -->
|
||||||
|
<div v-if="errorMessage" class="alert alert-danger mb-4" role="alert">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FLUSSO OTP -->
|
||||||
|
<div v-if="loginMethod === 'otp'">
|
||||||
|
|
||||||
|
<!-- Step 1: Inserisci Email -->
|
||||||
|
<form v-if="otpStep === 1" @submit.prevent="handleRequestOtp">
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary w-100" :disabled="isLoading">
|
||||||
|
<span v-if="isLoading" class="spinner-border spinner-border-sm me-2" role="status"></span>
|
||||||
|
Ricevi codice OTP
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Step 2: Inserisci Codice -->
|
||||||
|
<form v-else @submit.prevent="handleVerifyOtp">
|
||||||
|
<div class="alert alert-success mb-4 small">
|
||||||
|
Abbiamo inviato un codice OTP a <strong>{{ email }}</strong>.
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<input type="text" class="form-control text-center text-tracking-widest fs-4" id="otp" v-model="otpCode" required maxlength="6" autocomplete="one-time-code">
|
||||||
|
<label for="otp" :class="{ 'active': otpCode.length > 0 }">Codice a 6 cifre</label>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
Verifica e Accedi
|
||||||
|
</button>
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="#" class="small text-decoration-none" @click.prevent="otpStep = 1">Modifica indirizzo email</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FLUSSO PASSWORD (Fallback) -->
|
||||||
|
<form v-else @submit.prevent="handlePasswordLogin">
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<input type="email" class="form-control" id="emailPwd" v-model="email" required>
|
||||||
|
<label for="emailPwd" :class="{ 'active': email.length > 0 }">Indirizzo Email</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<input type="password" class="form-control" id="password" v-model="password" required>
|
||||||
|
<label for="password" :class="{ 'active': password.length > 0 }">Password</label>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
Accedi
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Switch Method -->
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<a href="#" class="small text-decoration-none" @click.prevent="loginMethod = loginMethod === 'otp' ? 'password' : 'otp'; errorMessage = ''">
|
||||||
|
{{ loginMethod === 'otp' ? 'Vuoi usare la password? Clicca qui' : 'Usa il login sicuro senza password (OTP)' }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<p class="small text-muted">Non hai un account? <NuxtLink to="/auth/register" class="font-weight-bold">Registrati ora</NuxtLink></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.text-tracking-widest {
|
||||||
|
letter-spacing: 0.5em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
+137
-4
@@ -1,11 +1,144 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
useHead({ title: 'Registrazione - Visione' })
|
||||||
|
|
||||||
|
const { register } = useAuth()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const successMessage = ref(false)
|
||||||
|
|
||||||
|
// Form State mappato sull'API RegisterCitizenRequest
|
||||||
|
const form = ref({
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
fiscal_code: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
phone: '',
|
||||||
|
privacy: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
if (!form.value.privacy) {
|
||||||
|
errorMessage.value = "È necessario accettare l'informativa sulla privacy."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
await register({
|
||||||
|
first_name: form.value.first_name,
|
||||||
|
last_name: form.value.last_name,
|
||||||
|
fiscal_code: form.value.fiscal_code.toUpperCase(),
|
||||||
|
email: form.value.email,
|
||||||
|
password: form.value.password,
|
||||||
|
phone: form.value.phone || undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
successMessage.value = true
|
||||||
|
// Ritarda il redirect per far leggere il messaggio
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/auth/login')
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage.value = err.data?.message || 'Errore durante la registrazione. Controlla i dati inseriti.'
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
$END$
|
<div class="d-flex align-items-center min-vh-100 bg-light py-5">
|
||||||
</template>
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-lg-8">
|
||||||
|
|
||||||
<style scoped>
|
<div class="text-center mb-4">
|
||||||
|
<BIIcon name="it-user" size="xl" color="primary" class="mb-2" />
|
||||||
|
<h1 class="h3 font-weight-bold">Registrazione Cittadino</h1>
|
||||||
|
<p class="text-muted">Crea il tuo profilo per iniziare a segnalare</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
</style>
|
<div class="card card-bg shadow-sm">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
|
||||||
|
<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...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="errorMessage" class="alert alert-danger mb-4" role="alert">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form v-if="!successMessage" @submit.prevent="handleRegister">
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<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 class="col-12 col-md-6 form-group mb-4">
|
||||||
|
<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 class="form-group mb-4">
|
||||||
|
<input type="text" class="form-control text-uppercase" id="cf" v-model="form.fiscal_code" required maxlength="16">
|
||||||
|
<label for="cf" :class="{ 'active': form.fiscal_code.length > 0 }">Codice Fiscale</label>
|
||||||
|
<small class="form-text text-muted">Sarà utilizzato come identificativo univoco nel sistema PA.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<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 class="col-12 col-md-6 form-group mb-4">
|
||||||
|
<input type="password" class="form-control" id="regPwd" v-model="form.password" required minlength="10">
|
||||||
|
<label for="regPwd" :class="{ 'active': form.password.length > 0 }">Password (min. 10 crt)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group mb-5">
|
||||||
|
<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>
|
||||||
|
<small class="form-text text-muted">Faciliterà il contatto da parte degli operatori comunali in caso di intervento.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Privacy Checkbox -->
|
||||||
|
<div class="form-check mb-4">
|
||||||
|
<input class="form-check-input" type="checkbox" id="privacyCheck" v-model="form.privacy" required>
|
||||||
|
<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.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary w-100 btn-lg" :disabled="isLoading">
|
||||||
|
<span v-if="isLoading" class="spinner-border spinner-border-sm me-2" role="status"></span>
|
||||||
|
Registra Profilo
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<p class="small text-muted">Hai già un account? <NuxtLink to="/auth/login" class="font-weight-bold">Torna al Login</NuxtLink></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user