From 23f668ae3f5b28005b85ef1ff4a06b6f40ce98a5 Mon Sep 17 00:00:00 2001 From: Yusron alamsyah Date: Tue, 24 Feb 2026 14:33:16 +0700 Subject: [PATCH] fix(FE) : fix store token --- .../full/vertical-sidebar/VerticalSidebar.vue | 48 ------------- data/mock/hakAkses.json | 37 ---------- server/api/auth/keycloak-callback.get.ts | 24 +++---- server/api/auth/session.get.ts | 6 +- server/api/auth/session.patch.ts | 71 +++++++++++++++++++ server/utils/sessionStore.ts | 16 +++++ services/api.ts | 33 +++++++++ 7 files changed, 134 insertions(+), 101 deletions(-) create mode 100644 server/api/auth/session.patch.ts diff --git a/components/layout/full/vertical-sidebar/VerticalSidebar.vue b/components/layout/full/vertical-sidebar/VerticalSidebar.vue index 49721ac..35f6fbd 100644 --- a/components/layout/full/vertical-sidebar/VerticalSidebar.vue +++ b/components/layout/full/vertical-sidebar/VerticalSidebar.vue @@ -7,57 +7,9 @@ import { Icon } from "@iconify/vue"; import { useRoute } from "vue-router"; // MiniSidebar Icons import MiniSideIcons from "./MinIconItems"; -import { useHakAkses } from "~/composables/useHakAkses"; const route = useRoute(); -const { getAllowedPages } = useHakAkses(); -const allowedPages = ref([]); -const isLoadingAccess = ref(true); - -// Fetch allowed pages on mount -onMounted(async () => { - try { - allowedPages.value = await getAllowedPages(); - console.log('Allowed pages for user:', allowedPages.value); - } catch (error) { - console.error('Error loading allowed pages:', error); - } finally { - isLoadingAccess.value = false; - } -}); - -// Filter sidebar items based on user's allowed pages -const filteredSidebarItems = computed(() => { - if (isLoadingAccess.value) { - return []; - } - - // If no allowed pages, return empty (no access) - if (allowedPages.value.length === 0) { - return []; - } - - return sidebarItems.map(section => { - // Filter children based on allowed pages - const filteredChildren = section.children?.filter(child => { - if (child.to) { - return allowedPages.value.includes(child.to); - } - return false; - }); - - // Only include section if it has visible children - if (filteredChildren && filteredChildren.length > 0) { - return { - ...section, - children: filteredChildren - }; - } - - return null; - }).filter(item => item !== null); -}); const findTitleByPath = (items: any, path: any) => { let title = ""; diff --git a/data/mock/hakAkses.json b/data/mock/hakAkses.json index a2ce62a..04f9abb 100644 --- a/data/mock/hakAkses.json +++ b/data/mock/hakAkses.json @@ -1,41 +1,4 @@ [ - { - "id": "9ce98d4d-2a5f-4ba4-ab1b-732ed6841bef", - "namaHakAkses": "manage-account", - "status": "tidak aktif", - "pages": [ - { - "id": "1", - "namaPage": "Dashboard", - "path": "/dashboard", - "icon": "solar:home-bold-duotone" - }, - { - "id": "2", - "namaPage": "Pengguna", - "path": "/users", - "icon": "solar:user-bold-duotone" - }, - { - "id": "3", - "namaPage": "Hak Akses", - "path": "/hakAkses", - "icon": "solar:lock-bold-duotone" - } - ] - }, - { - "id": "ceb0e9c4-f377-4060-ad39-800410132f89", - "namaHakAkses": "manage-account-links", - "status": "tidak aktif", - "pages": [] - }, - { - "id": "8d051ec2-1f42-4750-8ef2-11b2e15f819d", - "namaHakAkses": "view-profile", - "status": "tidak aktif", - "pages": [] - }, { "id": "d86a0a46-28ec-48bc-ae51-2679a61cbd83", "namaHakAkses": "superadmin", diff --git a/server/api/auth/keycloak-callback.get.ts b/server/api/auth/keycloak-callback.get.ts index 1cb5acf..91c39bb 100644 --- a/server/api/auth/keycloak-callback.get.ts +++ b/server/api/auth/keycloak-callback.get.ts @@ -129,6 +129,7 @@ export default defineEventHandler(async (event) => { let idTokenPayload; let accessTokenPayload; + let refreshTokenPayload; try { idTokenPayload = JSON.parse( Buffer.from(tokens.id_token.split('.')[1], 'base64').toString() @@ -136,23 +137,22 @@ export default defineEventHandler(async (event) => { accessTokenPayload = JSON.parse( Buffer.from(tokens.access_token.split('.')[1], 'base64').toString() ); + refreshTokenPayload = JSON.parse( + Buffer.from(tokens.refresh_token.split('.')[1], 'base64').toString() + ); } catch (decodeError) { - console.error('❌ Failed to decode ID token:', decodeError); - const errorMsg = encodeURIComponent('Invalid ID token format'); + console.error('❌ Failed to decode token:', decodeError); + const errorMsg = encodeURIComponent('Invalid token format'); return sendRedirect(event, `/auth/login?error=${errorMsg}`); } // Extract roles from Keycloak token // Keycloak stores roles in different places depending on configuration - const realmRoles = accessTokenPayload.realm_access?.roles || []; const clientRoles = accessTokenPayload.resource_access?.[config.keycloakClientId]?.roles || []; - const allRoles = [...new Set([...realmRoles, ...clientRoles])]; // Remove duplicates - - console.log('👥 User Roles Extracted:'); - console.log(' - Realm Roles:', realmRoles); - console.log(' - Client Roles:', clientRoles); - console.log(' - All Roles:', allRoles); + console.log("refreshTokenPayload.exp:", refreshTokenPayload.exp); + console.log("Current time (seconds):", Math.floor(Date.now() / 1000)); + console.log("Token expires in (seconds):", refreshTokenPayload.exp - Math.floor(Date.now() / 1000)); // Store minimal session data in cookie to reduce size // The ID token contains user info, so we can decode it when needed const sessionData = { @@ -162,8 +162,6 @@ export default defineEventHandler(async (event) => { email: idTokenPayload.email, name: idTokenPayload.name || idTokenPayload.preferred_username, preferred_username: idTokenPayload.preferred_username, - roles: allRoles, // All user roles combined - realm_roles: realmRoles, // Realm-specific roles client_roles: clientRoles, // Client-specific roles }, // Store tokens - these are necessary for API calls @@ -172,8 +170,8 @@ export default defineEventHandler(async (event) => { idToken: tokens.id_token, refreshToken: tokens.refresh_token, // Session metadata - expiresAt: Date.now() + (SESSION_DURATION * 1000), - createdAt: Date.now(), + expiresAt: refreshTokenPayload.exp * 1000, // Convert to milliseconds + createdAt: refreshTokenPayload.iat ? refreshTokenPayload.iat * 1000 : Date.now(), }; // Determine if we should use secure cookies diff --git a/server/api/auth/session.get.ts b/server/api/auth/session.get.ts index d77a814..4f56363 100644 --- a/server/api/auth/session.get.ts +++ b/server/api/auth/session.get.ts @@ -67,6 +67,7 @@ export default defineEventHandler(async (event) => { // Decode tokens and prepare the enhanced response data const idTokenPayload = decodeTokenPayload(session.idToken); const accessTokenPayload = decodeTokenPayload(session.accessToken); + const refreshTokenPayload = decodeTokenPayload(session.refreshToken); // Final response object - ensure it matches SessionResponse interface const sessionResponse: SessionResponse & { @@ -85,13 +86,12 @@ export default defineEventHandler(async (event) => { refreshToken: session.refreshToken, // Session Timestamps (optional in SessionResponse) - expiresAt: session.expiresAt, - + expiresAt: refreshTokenPayload?.exp ? refreshTokenPayload.exp * 1000 : session.expiresAt, // Additional debug fields (not in SessionResponse interface) idToken: session.idToken, idTokenPayload: idTokenPayload, accessTokenPayload: accessTokenPayload, - fullSessionObject: session, + // fullSessionObject: session, status: "authenticated", }; diff --git a/server/api/auth/session.patch.ts b/server/api/auth/session.patch.ts new file mode 100644 index 0000000..2e7a26b --- /dev/null +++ b/server/api/auth/session.patch.ts @@ -0,0 +1,71 @@ +// server/api/auth/session.patch.ts +export default defineEventHandler(async (event) => { + console.log("🔄 Session update endpoint called"); + + const sessionId = getCookie(event, "user_session"); + + if (!sessionId) { + console.log("❌ No session cookie found"); + throw createError({ + statusCode: 401, + statusMessage: "No session cookie found", + }); + } + + try { + // Get the update data from request body + const body = await readBody(event); + const { accessToken, idToken, refreshToken, expiresAt } = body; + + // Validate that at least one token is provided + if (!accessToken && !idToken && !refreshToken) { + throw createError({ + statusCode: 400, + statusMessage: "At least one token must be provided", + }); + } + + // Get session store functions + const { getSession, updateSession } = await import('~/server/utils/sessionStore'); + + // Verify session exists + const session = getSession(sessionId); + if (!session) { + console.log("❌ Session not found"); + deleteCookie(event, "user_session"); + throw createError({ + statusCode: 401, + statusMessage: "Session not found or expired", + }); + } + + // Prepare updates object + const updates: any = {}; + if (accessToken) updates.accessToken = accessToken; + if (idToken) updates.idToken = idToken; + if (refreshToken) updates.refreshToken = refreshToken; + if (expiresAt) updates.expiresAt = expiresAt; + + // Update the session + const updated = updateSession(sessionId, updates); + + if (!updated) { + throw createError({ + statusCode: 500, + statusMessage: "Failed to update session", + }); + } + + console.log("✅ Session updated successfully"); + return { + success: true, + message: "Session updated successfully", + }; + } catch (error: any) { + console.error("❌ Failed to update session:", error); + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.statusMessage || "Failed to update session", + }); + } +}); diff --git a/server/utils/sessionStore.ts b/server/utils/sessionStore.ts index 72bc92a..b0550c3 100644 --- a/server/utils/sessionStore.ts +++ b/server/utils/sessionStore.ts @@ -52,6 +52,22 @@ export function deleteSession(sessionId: string): void { sessions.delete(sessionId); } +export function updateSession(sessionId: string, updates: Partial): boolean { + const session = sessions.get(sessionId); + if (!session) { + return false; + } + + // Update the session with new data + const updatedSession = { + ...session, + ...updates, + }; + + sessions.set(sessionId, updatedSession); + return true; +} + // Helper function to get session from cookie (for use in API handlers) export async function getSessionFromCookie(event: any): Promise { const sessionId = getCookie(event, 'user_session'); diff --git a/services/api.ts b/services/api.ts index 4788326..dda9751 100644 --- a/services/api.ts +++ b/services/api.ts @@ -74,6 +74,39 @@ const refreshAccessToken = async (): Promise => { localStorage.setItem('refreshToken', data.refresh_token) } + // let refreshTokenPayload; + // try { + // refreshTokenPayload = JSON.parse( + // Buffer.from(data.refresh_token.split('.')[1], 'base64').toString() + // ); + // } catch (decodeError) { + // console.error('❌ Failed to decode refresh token:', decodeError); + // throw new Error('Invalid refresh token format'); + // } + + // const expiresAt = refreshTokenPayload.exp * 1000 + + + // Update server-side session with new tokens + try { + await fetch('/api/auth/session', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + accessToken: data.access_token, + idToken: data.id_token, + refreshToken: data.refresh_token, + // expiresAt: expiresAt + }), + }) + console.log('✅ Server session updated with new tokens') + } catch (sessionError) { + console.error('⚠️ Failed to update server session:', sessionError) + // Don't throw error here - token refresh was successful, session update is secondary + } + console.log('✅ Token refreshed successfully') return data.id_token || data.access_token } catch (error) {