feat: add CIE authentication callback page with polling mechanism for session verification
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
|
CieSessionResponse,
|
||||||
|
CieStartResponse,
|
||||||
CurrentUserResponse, EmailRequest, NotificationListResponse,
|
CurrentUserResponse, EmailRequest, NotificationListResponse,
|
||||||
OkResponse,
|
OkResponse,
|
||||||
OtpRequestResponse, RegisterCitizenRequest,
|
OtpRequestResponse, RegisterCitizenRequest,
|
||||||
@@ -82,6 +84,15 @@ export const useAuth = () => {
|
|||||||
const verifyOtp = (body: VerifyOtpRequest) =>
|
const verifyOtp = (body: VerifyOtpRequest) =>
|
||||||
useApiFetch<TokenResponse>('/api/v1/auth/otp/verify', { method: 'POST', body })
|
useApiFetch<TokenResponse>('/api/v1/auth/otp/verify', { method: 'POST', body })
|
||||||
|
|
||||||
|
const startCieLogin = (frontendRedirectUri: string) =>
|
||||||
|
useApiFetch<CieStartResponse>('/api/v1/auth/cie/start', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { frontend_redirect_uri: frontendRedirectUri }
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchCieSession = (id: string) =>
|
||||||
|
useApiFetch<CieSessionResponse>(`/api/v1/auth/cie/session/${id}`, { method: 'GET' })
|
||||||
|
|
||||||
const updateProfile = (body: UpdateProfileRequest) =>
|
const updateProfile = (body: UpdateProfileRequest) =>
|
||||||
useApiFetch<CurrentUserResponse>('/api/v1/me', { method: 'PATCH', body })
|
useApiFetch<CurrentUserResponse>('/api/v1/me', { method: 'PATCH', body })
|
||||||
|
|
||||||
@@ -110,6 +121,8 @@ export const useAuth = () => {
|
|||||||
register,
|
register,
|
||||||
requestOtp,
|
requestOtp,
|
||||||
verifyOtp,
|
verifyOtp,
|
||||||
|
startCieLogin,
|
||||||
|
fetchCieSession,
|
||||||
updateProfile,
|
updateProfile,
|
||||||
fetchCurrentUser,
|
fetchCurrentUser,
|
||||||
fetchRewards,
|
fetchRewards,
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'guest'
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({ title: 'Accesso CIE - Visione' })
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const { fetchCieSession, handleLoginSuccess } = useAuth()
|
||||||
|
|
||||||
|
const state = computed(() => {
|
||||||
|
const value = route.query.state
|
||||||
|
return typeof value === 'string' ? value : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLoading = ref(true)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const statusMessage = ref('Verifica dell’autenticazione CIE in corso...')
|
||||||
|
|
||||||
|
let pollTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let attempts = 0
|
||||||
|
const maxAttempts = 30
|
||||||
|
|
||||||
|
const stopPolling = () => {
|
||||||
|
if (pollTimer) {
|
||||||
|
clearTimeout(pollTimer)
|
||||||
|
pollTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleNextPoll = () => {
|
||||||
|
pollTimer = setTimeout(checkSession, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkSession = async () => {
|
||||||
|
if (!state.value) {
|
||||||
|
isLoading.value = false
|
||||||
|
errorMessage.value = 'Sessione CIE non valida: parametro state mancante.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts += 1
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetchCieSession(state.value)
|
||||||
|
|
||||||
|
if (res.status === 'authenticated' && res.auth?.token) {
|
||||||
|
stopPolling()
|
||||||
|
await handleLoginSuccess(res.auth.token)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['failed', 'expired', 'cancelled', 'error'].includes(res.status)) {
|
||||||
|
stopPolling()
|
||||||
|
isLoading.value = false
|
||||||
|
errorMessage.value = 'Autenticazione CIE non completata. Riprova dal login.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempts >= maxAttempts) {
|
||||||
|
stopPolling()
|
||||||
|
isLoading.value = false
|
||||||
|
errorMessage.value = 'Tempo di attesa scaduto durante la verifica CIE. Riprova.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
statusMessage.value = 'Autenticazione completata sul provider. Attendo conferma finale da Visione...'
|
||||||
|
scheduleNextPoll()
|
||||||
|
} catch (err: any) {
|
||||||
|
stopPolling()
|
||||||
|
isLoading.value = false
|
||||||
|
errorMessage.value = err.data?.message || 'Errore durante la verifica della sessione CIE.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkSession()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopPolling()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="d-flex align-items-center bg-light py-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-md-8 col-lg-6">
|
||||||
|
<div class="card card-bg shadow-sm">
|
||||||
|
<div class="card-body p-4 p-md-5 text-center">
|
||||||
|
<BIIcon name="it-card" size="xl" color="primary" class="mb-3" />
|
||||||
|
<h1 class="h3 font-weight-bold mb-3">Accesso con CIE</h1>
|
||||||
|
|
||||||
|
<div v-if="isLoading" class="py-3">
|
||||||
|
<div class="spinner-border text-primary mb-3" role="status">
|
||||||
|
<span class="visually-hidden">Caricamento...</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted mb-0">{{ statusMessage }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="errorMessage" class="py-2">
|
||||||
|
<div class="alert alert-danger text-start mb-4" role="alert">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" @click="router.push('/auth/login')">
|
||||||
|
Torna al login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -7,16 +7,20 @@ definePageMeta({
|
|||||||
|
|
||||||
useHead({ title: 'Accedi - Visione' })
|
useHead({ title: 'Accedi - Visione' })
|
||||||
|
|
||||||
const { requestOtp, verifyOtp, handleLoginSuccess } = useAuth()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
const { requestOtp, verifyOtp, handleLoginSuccess, startCieLogin } = useAuth()
|
||||||
|
|
||||||
// UI State
|
// UI State
|
||||||
const otpStep = ref<1 | 2>(1)
|
const otpStep = ref<1 | 2>(1)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
const isCieLoading = ref(false)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
|
||||||
const email = ref('')
|
const email = ref('')
|
||||||
const otpCode = ref('')
|
const otpCode = ref('')
|
||||||
|
|
||||||
|
const cieCallbackUrl = `${runtimeConfig.public.url}/auth/cie/callback`
|
||||||
|
|
||||||
const handleRequestOtp = async () => {
|
const handleRequestOtp = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
@@ -42,6 +46,21 @@ const handleVerifyOtp = async () => {
|
|||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCieLogin = async () => {
|
||||||
|
isCieLoading.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
try {
|
||||||
|
const res = await startCieLogin(cieCallbackUrl)
|
||||||
|
await navigateTo(res.authorization_url, {
|
||||||
|
external: true,
|
||||||
|
replace: true,
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage.value = err.data?.message || 'Avvio autenticazione CIE non riuscito. Riprova.'
|
||||||
|
isCieLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -61,9 +80,14 @@ const handleVerifyOtp = async () => {
|
|||||||
<div class="card-body p-4 p-md-5">
|
<div class="card-body p-4 p-md-5">
|
||||||
|
|
||||||
|
|
||||||
<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="isCieLoading || isLoading"
|
||||||
|
@click="handleCieLogin"
|
||||||
|
>
|
||||||
|
<span v-if="isCieLoading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
<BIIcon name="it-card" size="sm" color="white" />
|
<BIIcon name="it-card" size="sm" color="white" />
|
||||||
Entra con CIE (Presto disponibile)
|
Entra con CIE
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="d-flex align-items-center my-4">
|
<div class="d-flex align-items-center my-4">
|
||||||
|
|||||||
Vendored
+35
@@ -54,6 +54,41 @@ export interface TokenResponse {
|
|||||||
expires_in_hours: number;
|
expires_in_hours: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CieStartRequest {
|
||||||
|
frontend_redirect_uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CieStartResponse {
|
||||||
|
ok: boolean;
|
||||||
|
authorization_url: string;
|
||||||
|
state: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CieExchangeRequest {
|
||||||
|
state: string;
|
||||||
|
issuer: string;
|
||||||
|
subject: string;
|
||||||
|
fiscal_code: string;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CieSessionAuth {
|
||||||
|
token?: string;
|
||||||
|
token_type?: string;
|
||||||
|
expires_in_hours?: number;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CieSessionResponse {
|
||||||
|
ok: boolean;
|
||||||
|
status: string;
|
||||||
|
user_id?: number;
|
||||||
|
auth?: CieSessionAuth;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* REPORTS & MEDIA
|
* REPORTS & MEDIA
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user