From 7499ca60f60c345db98ce96dc50f5e96d8552310 Mon Sep 17 00:00:00 2001 From: ReLoia Date: Fri, 12 Jun 2026 19:04:19 +0200 Subject: [PATCH] feat: implement auth middleware, user sync, and token state persistence - Define useAuth composable logic with syncUser, handleLoginSuccess, and logout methods - Add auth.ts and guest.ts route middlewares to guard routes based on authentication and user roles - Add auth.client.ts plugin to boot-sync logged in user details - Declare custom PageMeta type definitions to support role-based authorization rules --- app/composables/useAuth.ts | 58 ++++++++++++++++++++++++++++++++++++++ app/middleware/auth.ts | 39 +++++++++++++++++++++++++ app/middleware/guest.ts | 17 +++++++++++ app/plugins/auth.client.ts | 16 +++++++++++ app/types/nuxt.d.ts | 8 ++++++ 5 files changed, 138 insertions(+) create mode 100644 app/middleware/auth.ts create mode 100644 app/middleware/guest.ts create mode 100644 app/plugins/auth.client.ts create mode 100644 app/types/nuxt.d.ts diff --git a/app/composables/useAuth.ts b/app/composables/useAuth.ts index f5e1951..872040f 100644 --- a/app/composables/useAuth.ts +++ b/app/composables/useAuth.ts @@ -8,6 +8,55 @@ import type { } from "~/types/api"; export const useAuth = () => { + const token = useCookie('auth_token', { + maxAge: 60 * 60 * 12, + sameSite: 'lax', + watch: true + }) + + const tokenState = useState('auth_token_state', () => token.value ?? null) + + type UserContext = CurrentUserResponse['user'] + const user = useState('auth_user', () => null) + const isSyncingUser = useState('auth_syncing', () => false) + const isAuthenticated = computed(() => !!(tokenState.value || token.value)) + + watch(() => token.value, (newVal) => { + tokenState.value = newVal || null + }) + + const syncUser = async () => { + const tokenVal = tokenState.value || token.value + if (!tokenVal || isSyncingUser.value) return + + isSyncingUser.value = true + try { + const res = await fetchCurrentUser() + if (res.ok) { + console.log('[Auth] User synced successfully') + user.value = res.user + } + } catch (error) { + console.error('[Auth] Sync failed, logging out:', error) + logout() + } finally { + isSyncingUser.value = false + } + } + + const handleLoginSuccess = async (jwtToken: string) => { + token.value = jwtToken + tokenState.value = jwtToken + await syncUser() + } + + const logout = () => { + token.value = null + tokenState.value = null + user.value = null + navigateTo('/auth/login') + } + const register = (body: RegisterCitizenRequest) => useApiFetch('/api/v1/auth/register', { method: 'POST', body }) @@ -36,6 +85,15 @@ export const useAuth = () => { useApiFetch(`/api/v1/notifications/${id}/read`, { method: 'PATCH' }) return { + user, + token, + tokenState, + isSyncingUser, + isAuthenticated, + handleLoginSuccess, + syncUser, + logout, + register, requestOtp, verifyOtp, diff --git a/app/middleware/auth.ts b/app/middleware/auth.ts new file mode 100644 index 0000000..be4dcd0 --- /dev/null +++ b/app/middleware/auth.ts @@ -0,0 +1,39 @@ +export default defineNuxtRouteMiddleware(async (to) => { + const auth = useAuth() + + // Se c'è un token ma l'utente non è ancora caricato e non stiamo già sincronizzando, sincronizza + if ((auth.token.value || auth.tokenState.value) && !auth.user.value && !auth.isSyncingUser.value) { + try { + await auth.syncUser() + } catch (error) { + console.error('[Auth Middleware] Sync failed:', error) + return navigateTo('/auth/login') + } + } + + // Se non autenticato, redirige al login + if (!auth.isAuthenticated.value) { + return navigateTo('/auth/login') + } + + // Verifica ruoli se richiesto + const requiredRole = to.meta.auth?.role + + if (requiredRole && auth.user.value) { + const userRole = auth.user.value.role + + const roleLevels: Record = { + 'CITIZEN': 1, + 'OPERATOR': 2, + 'ADMIN': 3 + } + + const userLevel = roleLevels[userRole] || 0 + const requiredLevel = roleLevels[requiredRole] || 0 + + if (userLevel < requiredLevel) { + console.warn(`[Auth] User role ${userRole} (level ${userLevel}) insufficient for required ${requiredRole} (level ${requiredLevel})`) + return navigateTo('/') + } + } +}) \ No newline at end of file diff --git a/app/middleware/guest.ts b/app/middleware/guest.ts new file mode 100644 index 0000000..9961132 --- /dev/null +++ b/app/middleware/guest.ts @@ -0,0 +1,17 @@ +export default defineNuxtRouteMiddleware(async () => { + const auth = useAuth() + + // Se c'è un token ma l'utente non è ancora caricato e non stiamo già sincronizzando, sincronizza + if ((auth.token.value || auth.tokenState.value) && !auth.user.value && !auth.isSyncingUser.value) { + try { + await auth.syncUser() + } catch (error) { + console.error('[Guest Middleware] Sync failed:', error) + } + } + + // Se autenticato, redirige al dashboard + if (auth.isAuthenticated.value && auth.user.value) { + return navigateTo('/dashboard') + } +}) \ No newline at end of file diff --git a/app/plugins/auth.client.ts b/app/plugins/auth.client.ts new file mode 100644 index 0000000..8c71dcf --- /dev/null +++ b/app/plugins/auth.client.ts @@ -0,0 +1,16 @@ +export default defineNuxtPlugin(async () => { + const auth = useAuth() + + if ((auth.token.value || auth.tokenState.value) && !auth.user.value) { + try { + await auth.syncUser() + } catch (error) { + console.error('[Auth Plugin] Sync failed:', error) + auth.logout() + } + } + + console.log('[Auth Plugin] Initialized - User:', auth.user.value?.id || 'None', 'Authenticated:', auth.isAuthenticated.value) +}) + + diff --git a/app/types/nuxt.d.ts b/app/types/nuxt.d.ts new file mode 100644 index 0000000..8c6e3f2 --- /dev/null +++ b/app/types/nuxt.d.ts @@ -0,0 +1,8 @@ +declare module '#app' { + interface PageMeta { + auth?: { + role?: 'CITIZEN' | 'OPERATOR' | 'ADMIN' + } + } +} +export {} \ No newline at end of file