From 0428017cacd68c3ab6e3ab236775932d449d3785 Mon Sep 17 00:00:00 2001 From: Fanrouver Date: Mon, 2 Feb 2026 14:47:00 +0700 Subject: [PATCH] upate session dan ws --- .env example | 2 + nuxt.config.ts | 5 + pages/Anjungan/AntreanMasuk/[id].vue | 120 ++++++++++++++++++----- pages/Anjungan/AntrianLoket/[id].vue | 81 +++++++++++++-- server/api/auth/keycloak-callback.get.ts | 13 +-- server/api/auth/keycloak-login.ts | 11 ++- server/api/auth/session.get.ts | 3 + 7 files changed, 193 insertions(+), 42 deletions(-) diff --git a/.env example b/.env example index 30250ca..73ad85a 100644 --- a/.env example +++ b/.env example @@ -32,6 +32,8 @@ NUXT_AUTH_SECRET="your-super-secret-string-of-at-least-32-characters" KEYCLOAK_CLIENT_ID="akbar-test" KEYCLOAK_CLIENT_SECRET="FDyv3UYMgJOYPnvzXVVv6diRtcgEevKg" KEYCLOAK_ISSUER="https://auth.rssa.top/realms/sandbox" +SESSION_DURATION_HOURS=1 +OAUTH_STATE_DURATION_MINUTES=10 #AUTH ORIGIN UNTUK ACCESS APLIKASI login dari KEYCLOAK # AUTH_ORIGIN="http://10.10.150.175:3000" diff --git a/nuxt.config.ts b/nuxt.config.ts index cef227b..fb6da14 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -67,6 +67,11 @@ export default defineNuxtConfig({ keycloakIssuer: process.env.KEYCLOAK_ISSUER, keycloakLogoutUri: process.env.KEYCLOAK_LOGOUT_URI, // Optional: custom logout URI postLogoutRedirectUri: process.env.POST_LOGOUT_REDIRECT_URI, // Optional: custom post-logout redirect URI + + // Session and Oauth durations + sessionDurationHours: parseInt(process.env.SESSION_DURATION_HOURS || '1', 10), + oauthStateDurationMinutes: parseInt(process.env.OAUTH_STATE_DURATION_MINUTES || '10', 10), + public: { authUrl: process.env.AUTH_ORIGIN, // authUrl: process.env.AUTH_ORIGIN || "http://10.10.150.175:3001", diff --git a/pages/Anjungan/AntreanMasuk/[id].vue b/pages/Anjungan/AntreanMasuk/[id].vue index e7226fd..90236e4 100644 --- a/pages/Anjungan/AntreanMasuk/[id].vue +++ b/pages/Anjungan/AntreanMasuk/[id].vue @@ -167,7 +167,8 @@ import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue' import { useQueueStore } from '@/stores/queueStore' import { useMasterStore } from '@/stores/masterStore' import { useAntreanMasukScreenStore } from '@/stores/antreanMasukScreenStore' -import { useRoute } from '#app' +import { useRoute, useRuntimeConfig } from '#app' +import { useWebSocket } from '@/composables/useWebSocket' definePageMeta({ layout: false, @@ -350,7 +351,16 @@ const displayedLokets = computed(() => { return String(q.loketId || 1) === String(loketId) }) .sort((a, b) => { - // Sort by createdAt or no for consistent ordering + // Sort numerically by ticket number (e.g., A1, A2, A10) + const noA = (a.noAntrian || '').split('|')[0].trim(); + const noB = (b.noAntrian || '').split('|')[0].trim(); + + // Use localeCompare with numeric: true for natural sorting (e.g., A1 before A10) + const numCompare = noA.localeCompare(noB, undefined, { numeric: true, sensitivity: 'base' }); + + if (numCompare !== 0) return numCompare; + + // Fallback to createdAt for consistent ordering if ticket numbers are identical if (a.createdAt && b.createdAt) { return new Date(a.createdAt) - new Date(b.createdAt) } @@ -468,6 +478,80 @@ const updateTime = () => { }) } +// ========== DATA FETCHING & WEBSOCKET ========== + +// Proactive Fetching: Ensure this screen fetches data even if others didn't +const fetchAllData = async () => { + if (!screenId.value) return; + + console.log('🔄 Anjungan refresh: Fetching data for configured lokets...'); + try { + // 1. Fetch data for each configured loket + const loketIds = configuredLoketIds.value; + if (loketIds && loketIds.length > 0) { + for (const id of loketIds) { + await queueStore.fetchPatientsForLoket(id); + } + } + + // 2. Ensure initial data (seeds etc) + queueStore.ensureInitialData(); + + console.log('✅ Anjungan refresh: Success'); + checkAllOverflows(); + } catch (err) { + console.error('❌ Anjungan refresh error:', err); + } +}; + +// WebSocket configuration for Anjungan +const config = useRuntimeConfig() +const wsBaseUrl = config.public?.wsBaseUrl || 'ws://10.10.150.100:8084/api/v1/ws' + +// Generate a unique session suffix (random ID) to fulfill "use different id" request +// This ensures that even if the same screen is opened in multiple tabs, they have distinct client IDs +const uniqueSessionSuffix = ref(process.client ? Math.random().toString(36).substring(2, 8) : '') + +const anjunganClientId = computed(() => { + if (!screenId.value) return '' + return `anjungan-masuk-${screenId.value}-${uniqueSessionSuffix.value}` +}) + +let wsInstance = null +const isConnected = ref(false) + +const initWebSocket = () => { + if (!screenId.value) return null + + console.log('🔌 WebSocket: Connecting to', wsBaseUrl) + console.log('🆔 Client ID:', anjunganClientId.value) + + wsInstance = useWebSocket({ + url: wsBaseUrl, + clientId: anjunganClientId.value, + fallbackPostUrl: '/stats-api/ws', + onMessage: (data) => { + console.log('📨 WebSocket message received:', data) + // Real-time update: refetch all relevant data when any message arrives + fetchAllData() + }, + onOpen: () => { + console.log('✅ WebSocket connected') + isConnected.value = true + }, + onClose: () => { + isConnected.value = false + }, + onError: () => { + isConnected.value = false + }, + reconnectInterval: 3000, + maxReconnectAttempts: 10 + }) + + return wsInstance +} + // Watch loketPatients to ensure reactivity after refresh watch(loketPatients, (newPatients) => { if (newPatients && newPatients.length > 0) { @@ -522,35 +606,23 @@ onMounted(() => { return } - // Proactive Fetching: Ensure this screen fetches data even if others didn't - const fetchAllData = async () => { - console.log('🔄 Anjungan polling: Fetching data for configured lokets...'); - try { - // 1. Fetch data for each configured loket - const loketIds = configuredLoketIds.value; - if (loketIds && loketIds.length > 0) { - for (const id of loketIds) { - await queueStore.fetchPatientsForLoket(id); - } - } - - // 2. Ensure initial data (seeds etc) - queueStore.ensureInitialData(); - - console.log('✅ Anjungan polling: Success'); - } catch (err) { - console.error('❌ Anjungan polling error:', err); - } - }; - // Initial fetch fetchAllData(); - // Polling every 10 seconds to keep data synchronized + // Polling every 10 seconds to keep data synchronized as fallback const pollingInterval = setInterval(fetchAllData, 10000); + + // Initialize and connect WebSocket + wsInstance = initWebSocket() + if (wsInstance) { + wsInstance.connect() + } onUnmounted(() => { clearInterval(pollingInterval); + if (wsInstance) { + wsInstance.disconnect() + } }); updateTime() diff --git a/pages/Anjungan/AntrianLoket/[id].vue b/pages/Anjungan/AntrianLoket/[id].vue index 425912e..bd223d6 100644 --- a/pages/Anjungan/AntrianLoket/[id].vue +++ b/pages/Anjungan/AntrianLoket/[id].vue @@ -190,12 +190,13 @@ diff --git a/server/api/auth/keycloak-callback.get.ts b/server/api/auth/keycloak-callback.get.ts index 691c085..81da336 100644 --- a/server/api/auth/keycloak-callback.get.ts +++ b/server/api/auth/keycloak-callback.get.ts @@ -1,12 +1,9 @@ -// server/api/auth/keycloak-callback.ts - EXTENDED SESSION FIX +const config = useRuntimeConfig(); +// Define session duration (default to 1 hour if not specified in config) +const SESSION_DURATION = (config.sessionDurationHours || 1) * 60 * 60; -// Add this at the top of the file (after imports) -const SESSION_DURATION = 1 * 60 * 60; // 1 hour in seconds (3600 seconds) -// Or use one of these alternatives: -// const SESSION_DURATION = 24 * 60 * 60; // 1 day -// const SESSION_DURATION = 30 * 24 * 60 * 60; // 30 days -// const SESSION_DURATION = 12 * 60 * 60; // 12 hours -// const SESSION_DURATION = 7 * 24 * 60 * 60; // 7 days +// This is the MAIN SESSION duration. It controls how long a user stays logged in. +// Current configuration: 1 hour (3600 seconds). export default defineEventHandler(async (event) => { try { diff --git a/server/api/auth/keycloak-login.ts b/server/api/auth/keycloak-login.ts index ed7633b..3bf6661 100644 --- a/server/api/auth/keycloak-login.ts +++ b/server/api/auth/keycloak-login.ts @@ -72,12 +72,17 @@ export default defineEventHandler(async (event) => { const state = randomBytes(32).toString('hex') console.log('🎲 Generated state:', state.substring(0, 8) + '...') + const oauthDuration = (config.oauthStateDurationMinutes || 10) * 60; // Default to 10 minutes + const isSecure = process.env.NODE_ENV === 'production'; + // Store state in session cookie + // IMPORTANT: This cookie is ONLY for the login flow (CSRF protection). + // It is NOT the main session duration. It expires quickly to ensure security. setCookie(event, 'oauth_state', state, { httpOnly: true, - secure: false, - sameSite: 'lax', - maxAge: 600 // 10 minutes + secure: isSecure, + sameSite: 'lax' as const, + maxAge: oauthDuration }) // Build Keycloak authorization URL diff --git a/server/api/auth/session.get.ts b/server/api/auth/session.get.ts index 6f57722..19ddb3b 100644 --- a/server/api/auth/session.get.ts +++ b/server/api/auth/session.get.ts @@ -74,6 +74,8 @@ export default defineEventHandler(async (event) => { accessTokenPayload?: any fullSessionObject?: any status?: string + remainingSeconds?: number + idToken?: string } = { success: true, // Basic User Info @@ -85,6 +87,7 @@ export default defineEventHandler(async (event) => { // Session Timestamps (optional in SessionResponse) expiresAt: session.expiresAt, + remainingSeconds: Math.max(0, Math.floor((session.expiresAt - Date.now()) / 1000)), // Additional debug fields (not in SessionResponse interface) idToken: session.idToken,