upate session dan ws

This commit is contained in:
Fanrouver
2026-02-02 14:47:00 +07:00
parent c899a71fd8
commit 0428017cac
7 changed files with 193 additions and 42 deletions
+2
View File
@@ -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"
+5
View File
@@ -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",
+96 -24
View File
@@ -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()
+74 -7
View File
@@ -190,12 +190,13 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useQueueStore } from '@/stores/queueStore'
import { useLoketStore } from '@/stores/loketStore'
import { useMasterStore } from '@/stores/masterStore'
import { useClinicStore } from '@/stores/clinicStore'
import { useRoute } from '#app'
import { useRoute, useRuntimeConfig } from '#app'
import { useWebSocket } from '@/composables/useWebSocket'
definePageMeta({
layout: false,
@@ -217,6 +218,10 @@ const loketStore = useLoketStore()
const masterStore = useMasterStore()
const clinicStore = useClinicStore()
// 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 currentTime = ref('')
const currentDate = ref('')
let timeInterval = null
@@ -853,14 +858,71 @@ const updateTime = () => {
// WebSocket configuration (Placeholder)
const config = useRuntimeConfig()
const wsBaseUrl = config.public?.wsBaseUrl || 'ws://10.10.150.100:8084/api/v1/ws'
const anjunganClientId = computed(() => `anjungan-loket-${loketId.value}`)
const anjunganClientId = computed(() => {
if (!loketId.value) return ''
return `anjungan-loket-${loketId.value}-${uniqueSessionSuffix.value}`
})
let wsInstance = null
const isConnected = ref(false)
const initWebSocket = () => {
console.log('🔌 WebSocket Placeholder: Connecting to', wsBaseUrl);
// Implementation will follow AntrianKlinikRuang style when backend is ready
return null;
if (!loketId.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)
// Handle both raw and wrapped data (matching AntreanMasuk style)
const messageData = data?.data || data
// If it's a patient call message, we should mimic the BroadcastChannel behavior
// The backend format might vary, but we look for 'noantrian' or 'patient'
if (messageData?.noantrian || messageData?.type === 'CALL_PATIENT') {
console.log('📞 WebSocket triggered call update');
// Refetch to ensure store is sync
queueStore.fetchPatientsForLoket(loketId.value);
// If message has full patient object, we can also update broadcastedPatient for instant Hero display
if (messageData.patient) {
broadcastedPatient.value = {
...messageData.patient,
_rawMessage: messageData
};
const targetId = String(loketId.value);
const key = targetId ? `loket-${targetId}` : 'loket';
if (queueStore.currentProcessingPatient) {
queueStore.currentProcessingPatient[key] = messageData.patient;
}
}
} else {
// General update: refetch current loket data
queueStore.fetchPatientsForLoket(loketId.value);
}
},
onOpen: () => {
console.log('✅ WebSocket connected')
isConnected.value = true
},
onClose: () => {
isConnected.value = false
},
onError: () => {
isConnected.value = false
},
reconnectInterval: 3000,
maxReconnectAttempts: 10
})
return wsInstance
}
onMounted(() => {
@@ -904,7 +966,11 @@ onMounted(() => {
console.error('❌ BroadcastChannel error:', e);
}
// Future: initWebSocket()
// Initialize and connect WebSocket
wsInstance = initWebSocket()
if (wsInstance) {
wsInstance.connect()
}
// Ensure initial data is loaded if store is empty
setTimeout(() => {
@@ -915,6 +981,7 @@ onMounted(() => {
onUnmounted(() => {
if (timeInterval) clearInterval(timeInterval)
if (broadcastChannel) broadcastChannel.close()
if (wsInstance) wsInstance.disconnect()
})
</script>
+5 -8
View File
@@ -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 {
+8 -3
View File
@@ -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
+3
View File
@@ -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,