Files
frontend/app/pages/auth/login.vue
T
reloiaandCopilot 82c5b12142 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]>
2026-06-10 23:20:51 +02:00

177 lines
6.9 KiB
Vue

<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>
<template>
<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>
<style scoped>
.text-tracking-widest {
letter-spacing: 0.5em;
}
</style>