import type { ComputedRef, Ref } from "vue"; import type { ExtendedUser } from "~/types/auth"; import { computed, ref, watch, readonly } from "vue"; export interface UseUserInfoReturnType { // State isAuthenticated: ComputedRef; isLoading: ComputedRef; user: ComputedRef; userRoles: ComputedRef; clientRoles: ComputedRef; accessToken: ComputedRef; sessionExpires: ComputedRef; isIdle: Ref; // Raw data for debugging data: Ref; status: Ref; // Session data from [...].ts idToken: ComputedRef; refreshToken: ComputedRef; accessTokenPayload: ComputedRef; expiresAt: ComputedRef; sessionScope: ComputedRef; jwtToken: ComputedRef; // Role checking functions hasRole: (role: string) => boolean; hasClientRole: (role: string) => boolean; hasAnyRole: (roles: string[]) => boolean; isAdmin: ComputedRef; // Session monitoring functions updateActivity: () => void; startSessionMonitoring: () => (() => void) | null; stopSessionMonitoring: () => void; // Authentication actions login: (provider?: string) => Promise; logout: (options?: any) => Promise; fullLogout: () => Promise; refresh: () => Promise; forceRefreshSession: () => Promise; isKeycloakSessionActive: () => Promise; } export const useUserInfo = (): UseUserInfoReturnType => { const { data, status, signIn, signOut, getSession } = useAuth(); // Internal writable refs for manual setting const internalData = ref(data.value); const internalStatus = ref(status.value); // Sync internal refs with useAuth data and status watch( () => data.value, (newVal) => { internalData.value = newVal; }, { immediate: true } ); watch( () => status.value, (newVal) => { internalStatus.value = newVal; }, { immediate: true } ); // Session monitoring state const lastActivity = ref(Date.now()); const isIdle = ref(false); const tabVisible = ref(true); // Constants const IDLE_TIMEOUT = 15 * 60 * 1000; // 15 menit const SESSION_CHECK_INTERVAL = 5 * 60 * 1000; // 5 menit const TAB_INACTIVE_THRESHOLD = 10 * 60 * 1000; // 10 menit // Basic computed properties const isAuthenticated = computed( () => internalStatus.value === "authenticated" ); const isLoading = computed(() => internalStatus.value === "loading"); // User info from session const user: ComputedRef = computed(() => { if (internalData.value?.user) { return internalData.value.user as ExtendedUser; } return null; }); // Session data dari [...].ts callbacks - INILAH DATA YANG AKAN DITAMPILKAN const idToken = computed(() => (internalData.value as any)?.id_token || ""); const accessToken = computed( () => (internalData.value as any)?.access_token || "" ); const refreshToken = computed( () => (internalData.value as any)?.refresh_token || "" ); const accessTokenPayload = computed( () => (internalData.value as any)?.access_token_payload || null ); const expiresAt = computed( () => (internalData.value as any)?.expires_at || null ); const sessionScope = computed(() => (internalData.value as any)?.scope || ""); const jwtToken = computed(() => (internalData.value as any)?.jwt || ""); // User roles dari session const userRoles = computed(() => { return (internalData.value?.user as any)?.roles || []; }); const clientRoles = computed(() => { return (internalData.value?.user as any)?.client_roles || []; }); // Session expires const sessionExpires = computed(() => data.value?.expires || null); // Activity tracking functions const updateActivity = () => { if (process.client) { lastActivity.value = Date.now(); isIdle.value = false; } }; const checkIdleStatus = () => { if (!process.client) return; const now = Date.now(); const timeSinceLastActivity = now - lastActivity.value; if ( timeSinceLastActivity >= IDLE_TIMEOUT && tabVisible.value && isAuthenticated.value ) { isIdle.value = true; handleIdleTimeout(); } }; const handleIdleTimeout = async () => { if (!process.client) return; try { const session = await getSession(); if ( session && session.expires && new Date(session.expires) > new Date() ) { await navigateTo("/auth/login?reason=idle&continue=true"); } else { await logout({ reason: "expired" }); } } catch (error) { console.error("Idle timeout handling error:", error); await logout({ reason: "expired" }); } }; const handleVisibilityChange = async () => { if (!process.client) return; tabVisible.value = !document.hidden; if (!document.hidden) { updateActivity(); const inactiveTime = Date.now() - lastActivity.value; if (inactiveTime >= TAB_INACTIVE_THRESHOLD && isAuthenticated.value) { await handleTabReactivation(); } } }; const handleTabReactivation = async () => { if (!process.client) return; try { const session = await getSession(); if ( session && session.expires && new Date(session.expires) > new Date() ) { await navigateTo("/auth/login?reason=tab_inactive&continue=true"); } else { await navigateTo("/auth/login?reason=session_expired"); } } catch (error) { console.error("Tab reactivation error:", error); await navigateTo("/auth/login?reason=error"); } }; const startSessionMonitoring = (): (() => void) | null => { if (!process.client) { console.warn("Session monitoring requires client-side environment"); return null; } if (!isAuthenticated.value) { console.warn("Session monitoring requires authenticated user"); return null; } const events = [ "mousedown", "mousemove", "keydown", "scroll", "touchstart", "click" ]; const activityHandler = () => updateActivity(); events.forEach((event) => { document.addEventListener(event, activityHandler, { passive: true }); }); document.addEventListener("visibilitychange", handleVisibilityChange); const idleInterval = setInterval(checkIdleStatus, 60 * 1000); const sessionInterval = setInterval(async () => { if (isAuthenticated.value) { try { await getSession(); } catch (error) { console.error("Session check failed:", error); clearInterval(sessionInterval); await logout({ reason: "expired" }); } } }, SESSION_CHECK_INTERVAL); return () => { events.forEach((event) => { document.removeEventListener(event, activityHandler); }); document.removeEventListener("visibilitychange", handleVisibilityChange); clearInterval(idleInterval); clearInterval(sessionInterval); }; }; // **PERBAIKAN: Stop Session Monitoring Function** const stopSessionMonitoring = (): void => { // Implementation bisa ditambahkan jika diperlukan //console.log("Stopping session monitoring..."); }; // Role checking functions const hasRole = (role: string): boolean => { return userRoles.value.includes(role); }; const hasClientRole = (role: string): boolean => { return clientRoles.value.includes(role); }; const hasAnyRole = (roles: string[]): boolean => { return roles.some((role) => userRoles.value.includes(role)); }; const isAdmin = computed( () => hasRole("admin") || hasRole("super_admin") || hasRole("realm-admin") ); // Login function const login = async (provider = "keycloak") => { try { const currentUrl = window.location.pathname; const callbackUrl = currentUrl !== "/auth/login" ? currentUrl : "/"; const result = await signIn(provider, { callbackUrl, redirect: true }); if (result?.error) { throw new Error(result.error); } // **PERBAIKAN: Update activity setelah login berhasil** updateActivity(); await getSession(); } catch (error) { throw error; } }; // Enhanced Logout function dengan reason support const logout = async (options?: { redirect?: string; clearStorage?: boolean; confirmDialog?: boolean; reason?: "manual" | "expired" | "admin" | "idle"; }) => { const defaultOptions = { redirect: "/auth/login", clearStorage: true, confirmDialog: false, reason: "idle" as const, ...options }; try { //console.log("Logout called with options:", defaultOptions); // Menghilangkan pemanggilan fullLogout manual sesuai permintaan if (defaultOptions.clearStorage && process.client) { //console.log("Clearing localStorage and sessionStorage"); localStorage.removeItem("user-preferences"); localStorage.removeItem("app-state"); sessionStorage.clear(); } // Memanggil API logout server untuk menghandle logout Keycloak dan pembersihan session if (process.client) { // Mengambil keycloakIssuer dan redirectUri dari public runtime config const config = useRuntimeConfig(); const keycloakIssuer = config.public.keycloakIssuer; const origin = window.location.origin; const redirectUri = `${origin}/auth/login`; let logoutUrl = `${keycloakIssuer}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent( redirectUri )}`; if (idToken.value) { logoutUrl += `&id_token_hint=${encodeURIComponent(idToken.value)}`; } //console.log("Calling Keycloak logout URL:", logoutUrl); await fetch("/api/auth/logout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id_token_hint: idToken.value }) }); // Bersihkan local storage setelah logout server //console.log("Clearing localStorage and sessionStorage after server logout"); localStorage.removeItem("user-preferences"); localStorage.removeItem("app-state"); sessionStorage.clear(); } await signOut({ callbackUrl: `${defaultOptions.redirect}?reason=idle`, redirect: true }); } catch (error) { console.error("Logout failed:", error); throw new Error("Logout failed. Please try again."); } }; // Perbaikan fullLogout - simplified dan menggunakan logout yang sudah ada const fullLogout = async () => { try { console.log("Full logout initiated"); // 1. Stop session monitoring terlebih dahulu stopSessionMonitoring(); console.log("Session monitoring stopped"); console.log(process); if (process.client) { // 2. Clear all storage before logout console.log("Clearing all storage before logout"); // Clear localStorage localStorage.removeItem("user-preferences"); localStorage.removeItem("app-state"); localStorage.clear(); // Clear sessionStorage sessionStorage.clear(); // Log cookies and sessionStorage before clearing console.log("Cookies before clearing:", document.cookie); console.log( "SessionStorage before clearing:", JSON.stringify(sessionStorage) ); // Clear cookies if any const cookies = document.cookie.split(";"); const domain = window.location.hostname; cookies.forEach((c, index) => { const eqPos = c.indexOf("="); const name = eqPos > -1 ? c.substr(0, eqPos).trim() : c.trim(); console.log(`Clearing cookie [${index}]:`, name); // Clear cookie for current path document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/"; // Clear cookie for current path and domain document.cookie = name + `=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domain}`; // Clear cookie for current path and domain with leading dot (for subdomains) if (domain.indexOf(".") !== -1) { const domainWithDot = "." + domain; document.cookie = name + `=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domainWithDot}`; } // Explicitly clear Keycloak cookies if present const keycloakCookies = [ "KEYCLOAK_SESSION", "KEYCLOAK_IDENTITY", "KEYCLOAK_IDENTITY_LEGACY", "KEYCLOAK_REMEMBER_ME", "KEYCLOAK_REMEMBER_ME_LEGACY" ]; if (keycloakCookies.includes(name)) { console.log(`Explicitly clearing Keycloak cookie: ${name}`); document.cookie = name + `=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domain}`; if (domain.indexOf(".") !== -1) { const domainWithDot = "." + domain; document.cookie = name + `=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domainWithDot}`; } } console.log("Cleared cookie:", name); }); } // 3. Call server API to get the correct logout URL const response = await $fetch("/api/auth/logout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id_token_hint: idToken.value }) }); if (response.success && response.logoutUrl && process.client) { console.log("Redirecting to Keycloak logout:", response.logoutUrl); localStorage.removeItem("user-preferences"); localStorage.removeItem("app-state"); localStorage.clear(); sessionStorage.clear(); // Append query parameters to logoutUrl let logoutUrlWithParams = response.logoutUrl; // 4. Redirect directly to Keycloak logout endpoint with appended params window.location.href = logoutUrlWithParams; return; } // Fallback to signOut if server API fails // Clear local data and update status to logged out if (process.client) { // Clear localStorage and sessionStorage again to be sure localStorage.clear(); sessionStorage.clear(); // Reset data and status to reflect logged out state internalData.value = null; internalStatus.value = "unauthenticated"; } return; } catch (error) { console.error("Full logout failed:", error); // Emergency fallback - manual logout to Keycloak if (process.client) { try { console.log("Executing emergency logout"); const config = useRuntimeConfig(); const keycloakIssuer = config.public.keycloakIssuer; const origin = window.location.origin; const redirectUri = `${origin}/auth/login?reason=emergency`; // Clear all storage again to ensure localStorage.clear(); sessionStorage.clear(); // Build logout URL with client_id as fallback let emergencyLogoutUrl = `${keycloakIssuer}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent( redirectUri )}`; // Add id_token_hint if available if (idToken.value) { emergencyLogoutUrl += `&id_token_hint=${encodeURIComponent( idToken.value )}`; } else { // Use client_id as alternative if id_token_hint is missing const clientId = config.public.keycloakClientId || "your-client-id"; emergencyLogoutUrl += `&client_id=${encodeURIComponent( String(clientId) )}`; } console.log("Emergency logout URL:", emergencyLogoutUrl); window.location.href = emergencyLogoutUrl; } catch (emergencyError) { console.error("Emergency logout failed:", emergencyError); // Last resort - force redirect and clear storage if (process.client) { localStorage.clear(); sessionStorage.clear(); window.location.href = "/auth/login?reason=force"; } } } } }; // Force refresh session const forceRefreshSession = async () => { try { await getSession(); } catch (error) { console.error("Force refresh session error:", error); } }; // New method to check if Keycloak session is active const isKeycloakSessionActive = async (): Promise => { if (!process.client) return false; try { const session = await getSession(); if ( session && session.expires && new Date(session.expires) > new Date() ) { return true; } return false; } catch (error) { console.error("Error checking Keycloak session active status:", error); return false; } }; // Add computed property for accessTokenPayload // **PERBAIKAN: Return semua fungsi yang diperlukan termasuk session monitoring** return { // State isAuthenticated, isLoading, user, userRoles, clientRoles, accessToken, sessionExpires, isIdle: readonly(isIdle), // Raw data untuk debugging data: internalData, status: internalStatus, // Session data dari [...].ts idToken, refreshToken, accessTokenPayload, expiresAt, sessionScope, jwtToken, // Role checking functions hasRole, hasClientRole, hasAnyRole, isAdmin, // Session monitoring functions updateActivity, startSessionMonitoring, stopSessionMonitoring, // Authentication actions login, logout, fullLogout, refresh: async () => { await getSession(); }, forceRefreshSession, isKeycloakSessionActive }; };