upate session dan ws
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user