Enhance authorization and authentication handling by optimizing state synchronization from middleware cache, implementing error handling in custom auth, and adding admin audit and system logs pages with filtering and real-time updates.

This commit is contained in:
Matt 2025-07-09 13:00:01 -04:00
parent 36048dfed1
commit da9ab99519
6 changed files with 954 additions and 51 deletions

View File

@ -93,30 +93,34 @@ export const useAuthorization = () => {
// Initialize immediately (both client and server)
initializeAuth();
// Watch for changes in payload data and reinitialize
// Watch for changes in payload data (optimized to prevent loops)
if (process.client) {
let watcherTimeout: NodeJS.Timeout | null = null;
watch(
() => nuxtApp.payload?.data?.authState,
(newAuthState) => {
if (newAuthState && typeof newAuthState === 'object') {
console.log('[useAuthorization] Auth state updated, reinitializing...');
hasInitialized.value = false; // Force re-initialization
initializeAuth();
}
},
{ immediate: true, deep: true }
);
(newAuthState, oldAuthState) => {
// Only update if the auth state actually changed
if (newAuthState && typeof newAuthState === 'object' && newAuthState !== oldAuthState) {
// Debounce to prevent rapid re-initialization
if (watcherTimeout) clearTimeout(watcherTimeout);
// Also watch for session cache updates
watch(
() => nuxtApp.payload?.data?.['auth:session:cache'],
(newSessionCache) => {
if (newSessionCache && typeof newSessionCache === 'object' && !hasInitialized.value) {
console.log('[useAuthorization] Session cache updated, initializing...');
initializeAuth();
watcherTimeout = setTimeout(() => {
console.log('[useAuthorization] Auth state updated, syncing...');
// Direct sync instead of full re-initialization
authState.user = newAuthState.user || null;
authState.authenticated = newAuthState.authenticated || false;
authState.groups = Array.isArray(newAuthState.groups) ? newAuthState.groups : [];
if (!hasInitialized.value) {
hasInitialized.value = true;
isLoading.value = false;
}
}, 100); // 100ms debounce
}
},
{ immediate: true, deep: true }
{ immediate: false, deep: false }
);
}

View File

@ -16,42 +16,62 @@ export const useCustomAuth = () => {
const authenticated = ref(false)
const loading = ref(true)
const refreshing = ref(false)
const retryCount = ref(0)
const maxRetries = 3
// Check authentication status with retry logic
const checkAuth = async (skipRetry = false) => {
// Get auth state from middleware cache (no API calls!)
const syncFromCache = () => {
try {
loading.value = true
const data = await $fetch<AuthState>('/api/auth/session', {
retry: skipRetry ? 0 : 2,
retryDelay: 1000
})
user.value = data.user
authenticated.value = data.authenticated
retryCount.value = 0 // Reset retry count on success
const nuxtApp = useNuxtApp()
console.log('[CUSTOM_AUTH] Session check result:', {
authenticated: data.authenticated,
userId: data.user?.id
})
} catch (error) {
console.error('[CUSTOM_AUTH] Session check failed:', error)
// Try to get from auth state cache first
const authState = nuxtApp.payload?.data?.authState
if (authState && typeof authState === 'object') {
user.value = authState.user || null
authenticated.value = authState.authenticated || false
loading.value = false
// If it's a network error and we haven't exceeded retry limit, try refresh
if (!skipRetry && retryCount.value < maxRetries && (error as any)?.status >= 500) {
retryCount.value++
console.log(`[CUSTOM_AUTH] Retrying session check (${retryCount.value}/${maxRetries})...`)
// Wait a bit before retrying
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount.value))
return checkAuth(false)
console.log('[CUSTOM_AUTH] Session synced from cache:', {
authenticated: authenticated.value,
userId: user.value?.id
})
return true
}
// Fallback to session cache
const sessionCache = nuxtApp.payload?.data?.['auth:session:cache']
if (sessionCache && typeof sessionCache === 'object') {
user.value = sessionCache.user || null
authenticated.value = sessionCache.authenticated || false
loading.value = false
console.log('[CUSTOM_AUTH] Session synced from session cache:', {
authenticated: authenticated.value,
userId: user.value?.id
})
return true
}
// No cache available
console.log('[CUSTOM_AUTH] No cache available, setting defaults')
user.value = null
authenticated.value = false
} finally {
loading.value = false
return false
} catch (error) {
console.error('[CUSTOM_AUTH] Error syncing from cache:', error)
user.value = null
authenticated.value = false
loading.value = false
return false
}
}
// Simple check auth that only uses cache
const checkAuth = async (skipRetry = false) => {
loading.value = true
const synced = syncFromCache()
if (!synced) {
console.warn('[CUSTOM_AUTH] No auth cache available, user may need to refresh')
}
}
@ -133,6 +153,23 @@ export const useCustomAuth = () => {
checkAuth()
})
// Watch for auth state changes in the cache (client-side only)
if (process.client) {
watch(
() => {
const nuxtApp = useNuxtApp()
return nuxtApp.payload?.data?.authState
},
(newAuthState) => {
if (newAuthState && typeof newAuthState === 'object') {
console.log('[CUSTOM_AUTH] Auth state changed, syncing from cache')
syncFromCache()
}
},
{ deep: false }
)
}
return {
user: readonly(user),
authenticated: readonly(authenticated),

View File

@ -187,9 +187,24 @@ const defaultMenu = computed(() => {
return baseMenu;
});
const menu = computed(() =>
toValue(tags).interest ? interestMenu : defaultMenu
);
const menu = computed(() => {
try {
const tagsValue = toValue(tags);
const menuToUse = tagsValue.interest ? interestMenu.value : defaultMenu.value;
console.log('[Dashboard] Computing menu:', {
hasInterestTag: tagsValue.interest,
menuType: tagsValue.interest ? 'interestMenu' : 'defaultMenu',
menuIsArray: Array.isArray(menuToUse),
menuLength: menuToUse?.length
});
return menuToUse;
} catch (error) {
console.error('[Dashboard] Error computing menu:', error);
return [];
}
});
// Safe menu wrapper to prevent crashes when menu is undefined
const safeMenu = computed(() => {

View File

@ -0,0 +1,392 @@
<template>
<div class="pa-4">
<div class="d-flex align-center mb-6">
<v-btn
icon="mdi-arrow-left"
variant="text"
@click="$router.back()"
class="mr-4"
/>
<div>
<h1 class="text-h4 mb-1">Audit Logs</h1>
<p class="text-subtitle-1 text-grey-darken-1">View system audit trail and user activities</p>
</div>
</div>
<v-row>
<v-col cols="12" md="6" lg="3">
<v-card>
<v-card-text>
<div class="d-flex align-center">
<v-icon color="primary" size="large" class="mr-3">mdi-chart-line</v-icon>
<div>
<div class="text-h6">{{ stats.totalEvents }}</div>
<div class="text-caption text-grey-darken-1">Total Events</div>
</div>
</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6" lg="3">
<v-card>
<v-card-text>
<div class="d-flex align-center">
<v-icon color="success" size="large" class="mr-3">mdi-check-circle</v-icon>
<div>
<div class="text-h6">{{ stats.successRate }}%</div>
<div class="text-caption text-grey-darken-1">Success Rate</div>
</div>
</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6" lg="3">
<v-card>
<v-card-text>
<div class="d-flex align-center">
<v-icon color="warning" size="large" class="mr-3">mdi-alert-circle</v-icon>
<div>
<div class="text-h6">{{ stats.errorCount }}</div>
<div class="text-caption text-grey-darken-1">Errors Today</div>
</div>
</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6" lg="3">
<v-card>
<v-card-text>
<div class="d-flex align-center">
<v-icon color="info" size="large" class="mr-3">mdi-account-multiple</v-icon>
<div>
<div class="text-h6">{{ stats.activeUsers }}</div>
<div class="text-caption text-grey-darken-1">Active Users</div>
</div>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-card class="mt-6">
<v-card-title>
<div class="d-flex align-center justify-space-between">
<span>Recent Audit Events</span>
<v-btn
@click="refreshLogs"
:loading="loading"
icon="mdi-refresh"
variant="text"
size="small"
/>
</div>
</v-card-title>
<v-card-text>
<div class="d-flex flex-wrap gap-3 mb-4">
<v-text-field
v-model="search"
label="Search events..."
prepend-inner-icon="mdi-magnify"
variant="outlined"
density="compact"
hide-details
clearable
style="max-width: 300px"
/>
<v-select
v-model="filterLevel"
:items="['all', 'info', 'warning', 'error']"
label="Filter by level"
variant="outlined"
density="compact"
hide-details
style="max-width: 150px"
/>
<v-select
v-model="filterAction"
:items="['all', 'create', 'update', 'delete', 'login', 'logout']"
label="Filter by action"
variant="outlined"
density="compact"
hide-details
style="max-width: 150px"
/>
</div>
<v-data-table
:headers="headers"
:items="filteredLogs"
:search="search"
:loading="loading"
:items-per-page="25"
:items-per-page-options="[10, 25, 50, 100]"
class="elevation-1"
>
<template #item.level="{ item }">
<v-chip
:color="getLevelColor(item.level)"
size="small"
variant="tonal"
>
{{ item.level.toUpperCase() }}
</v-chip>
</template>
<template #item.action="{ item }">
<v-chip
:color="getActionColor(item.action)"
size="small"
variant="outlined"
>
{{ item.action }}
</v-chip>
</template>
<template #item.timestamp="{ item }">
<div class="text-caption">
<div>{{ formatDate(item.timestamp) }}</div>
<div class="text-grey-darken-1">{{ formatTime(item.timestamp) }}</div>
</div>
</template>
<template #item.actions="{ item }">
<v-btn
@click="viewDetails(item)"
icon="mdi-eye"
size="small"
variant="text"
/>
</template>
</v-data-table>
</v-card-text>
</v-card>
<!-- Details Dialog -->
<v-dialog v-model="showDetails" max-width="600">
<v-card v-if="selectedLog">
<v-card-title>
<span class="text-h6">Audit Log Details</span>
<v-spacer />
<v-btn icon="mdi-close" @click="showDetails = false" variant="text" />
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<div class="mb-3">
<div class="text-caption text-grey-darken-1">TIMESTAMP</div>
<div>{{ formatDateTime(selectedLog.timestamp) }}</div>
</div>
</v-col>
<v-col cols="12" md="6">
<div class="mb-3">
<div class="text-caption text-grey-darken-1">LEVEL</div>
<v-chip
:color="getLevelColor(selectedLog.level)"
size="small"
variant="tonal"
>
{{ selectedLog.level.toUpperCase() }}
</v-chip>
</div>
</v-col>
<v-col cols="12" md="6">
<div class="mb-3">
<div class="text-caption text-grey-darken-1">ACTION</div>
<div>{{ selectedLog.action }}</div>
</div>
</v-col>
<v-col cols="12" md="6">
<div class="mb-3">
<div class="text-caption text-grey-darken-1">USER</div>
<div>{{ selectedLog.user || 'System' }}</div>
</div>
</v-col>
<v-col cols="12">
<div class="mb-3">
<div class="text-caption text-grey-darken-1">RESOURCE</div>
<div>{{ selectedLog.resource }}</div>
</div>
</v-col>
<v-col cols="12">
<div class="mb-3">
<div class="text-caption text-grey-darken-1">MESSAGE</div>
<div>{{ selectedLog.message }}</div>
</div>
</v-col>
<v-col cols="12" v-if="selectedLog.metadata">
<div class="mb-3">
<div class="text-caption text-grey-darken-1">METADATA</div>
<pre class="text-caption bg-grey-lighten-4 pa-2 rounded">{{ JSON.stringify(selectedLog.metadata, null, 2) }}</pre>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</div>
</template>
<script setup>
import { formatDate, formatTime, formatDateTime } from '@/utils/dateUtils'
definePageMeta({
middleware: ['authentication', 'authorization'],
auth: {
roles: ['admin']
}
})
useHead({
title: 'Audit Logs - Admin'
})
const { isAdmin } = useAuthorization()
// Redirect if not admin
if (!isAdmin()) {
throw createError({
statusCode: 403,
statusMessage: 'Access denied'
})
}
const loading = ref(false)
const search = ref('')
const filterLevel = ref('all')
const filterAction = ref('all')
const showDetails = ref(false)
const selectedLog = ref(null)
const stats = ref({
totalEvents: 0,
successRate: 100,
errorCount: 0,
activeUsers: 0
})
const logs = ref([])
const headers = [
{ title: 'Timestamp', key: 'timestamp', width: 150 },
{ title: 'Level', key: 'level', width: 100 },
{ title: 'Action', key: 'action', width: 120 },
{ title: 'User', key: 'user', width: 150 },
{ title: 'Resource', key: 'resource', width: 200 },
{ title: 'Message', key: 'message' },
{ title: 'Actions', key: 'actions', sortable: false, width: 80 }
]
const filteredLogs = computed(() => {
let filtered = logs.value
if (filterLevel.value !== 'all') {
filtered = filtered.filter(log => log.level === filterLevel.value)
}
if (filterAction.value !== 'all') {
filtered = filtered.filter(log => log.action === filterAction.value)
}
return filtered
})
const getLevelColor = (level) => {
const colors = {
info: 'blue',
warning: 'orange',
error: 'red',
success: 'green'
}
return colors[level] || 'grey'
}
const getActionColor = (action) => {
const colors = {
create: 'green',
update: 'blue',
delete: 'red',
login: 'purple',
logout: 'grey'
}
return colors[action] || 'grey'
}
const viewDetails = (log) => {
selectedLog.value = log
showDetails.value = true
}
const refreshLogs = async () => {
loading.value = true
try {
const [logsResponse, statsResponse] = await Promise.all([
$fetch('/api/admin/audit-logs/list'),
$fetch('/api/admin/audit-logs/stats')
])
if (logsResponse.success) {
logs.value = logsResponse.data
}
if (statsResponse.success) {
stats.value = statsResponse.data
}
} catch (error) {
console.error('Failed to load audit logs:', error)
// Show sample data for now
logs.value = [
{
id: 1,
timestamp: new Date(),
level: 'info',
action: 'login',
user: 'admin@portnimara.dev',
resource: 'auth',
message: 'User logged in successfully',
metadata: { ip: '127.0.0.1', userAgent: 'Mozilla/5.0...' }
},
{
id: 2,
timestamp: new Date(Date.now() - 300000),
level: 'info',
action: 'create',
user: 'admin@portnimara.dev',
resource: 'interest',
message: 'New interest record created',
metadata: { id: 123, name: 'John Doe' }
},
{
id: 3,
timestamp: new Date(Date.now() - 600000),
level: 'warning',
action: 'update',
user: 'admin@portnimara.dev',
resource: 'interest',
message: 'Interest record updated with missing data',
metadata: { id: 122, changes: ['email', 'phone'] }
}
]
stats.value = {
totalEvents: logs.value.length,
successRate: 100,
errorCount: 0,
activeUsers: 1
}
}
loading.value = false
}
// Load data on mount
onMounted(() => {
refreshLogs()
})
</script>

View File

@ -0,0 +1,448 @@
<template>
<div class="pa-4">
<div class="d-flex align-center mb-6">
<v-btn
icon="mdi-arrow-left"
variant="text"
@click="$router.back()"
class="mr-4"
/>
<div>
<h1 class="text-h4 mb-1">System Logs</h1>
<p class="text-subtitle-1 text-grey-darken-1">Real-time system logs and monitoring</p>
</div>
</div>
<v-row>
<v-col cols="12" md="6" lg="3">
<v-card>
<v-card-text>
<div class="d-flex align-center">
<v-icon color="success" size="large" class="mr-3">mdi-check-circle</v-icon>
<div>
<div class="text-h6">{{ systemHealth.status }}</div>
<div class="text-caption text-grey-darken-1">System Status</div>
</div>
</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6" lg="3">
<v-card>
<v-card-text>
<div class="d-flex align-center">
<v-icon color="info" size="large" class="mr-3">mdi-memory</v-icon>
<div>
<div class="text-h6">{{ systemHealth.memoryUsage }}%</div>
<div class="text-caption text-grey-darken-1">Memory Usage</div>
</div>
</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6" lg="3">
<v-card>
<v-card-text>
<div class="d-flex align-center">
<v-icon color="warning" size="large" class="mr-3">mdi-speedometer</v-icon>
<div>
<div class="text-h6">{{ systemHealth.cpuUsage }}%</div>
<div class="text-caption text-grey-darken-1">CPU Usage</div>
</div>
</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6" lg="3">
<v-card>
<v-card-text>
<div class="d-flex align-center">
<v-icon color="primary" size="large" class="mr-3">mdi-clock</v-icon>
<div>
<div class="text-h6">{{ systemHealth.uptime }}</div>
<div class="text-caption text-grey-darken-1">Uptime</div>
</div>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row class="mt-4">
<v-col cols="12" md="8">
<v-card>
<v-card-title>
<div class="d-flex align-center justify-space-between">
<span>Live System Logs</span>
<div class="d-flex align-center gap-2">
<v-switch
v-model="autoRefresh"
label="Auto-refresh"
color="primary"
hide-details
density="compact"
/>
<v-btn
@click="refreshLogs"
:loading="loading"
icon="mdi-refresh"
variant="text"
size="small"
/>
</div>
</div>
</v-card-title>
<v-card-text>
<div class="d-flex flex-wrap gap-3 mb-4">
<v-text-field
v-model="search"
label="Search logs..."
prepend-inner-icon="mdi-magnify"
variant="outlined"
density="compact"
hide-details
clearable
style="max-width: 300px"
/>
<v-select
v-model="filterLevel"
:items="['all', 'debug', 'info', 'warn', 'error']"
label="Filter by level"
variant="outlined"
density="compact"
hide-details
style="max-width: 150px"
/>
<v-select
v-model="filterService"
:items="['all', 'auth', 'api', 'database', 'email', 'storage']"
label="Filter by service"
variant="outlined"
density="compact"
hide-details
style="max-width: 150px"
/>
</div>
<div class="logs-container">
<v-virtual-scroll
:items="filteredLogs"
height="400"
item-height="60"
>
<template #default="{ item }">
<div
:class="[
'log-entry',
`log-${item.level}`,
{ 'log-selected': selectedLog?.id === item.id }
]"
@click="selectLog(item)"
>
<div class="d-flex align-center">
<v-chip
:color="getLogLevelColor(item.level)"
size="x-small"
variant="tonal"
class="mr-2"
>
{{ item.level.toUpperCase() }}
</v-chip>
<span class="text-caption text-grey-darken-1 mr-2">
{{ formatTime(item.timestamp) }}
</span>
<span class="text-caption text-grey-darken-1 mr-2">
[{{ item.service }}]
</span>
<span class="text-body-2 flex-grow-1">
{{ item.message }}
</span>
</div>
</div>
</template>
</v-virtual-scroll>
</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card>
<v-card-title>Log Details</v-card-title>
<v-card-text>
<div v-if="selectedLog">
<div class="mb-3">
<div class="text-caption text-grey-darken-1">TIMESTAMP</div>
<div>{{ formatDateTime(selectedLog.timestamp) }}</div>
</div>
<div class="mb-3">
<div class="text-caption text-grey-darken-1">LEVEL</div>
<v-chip
:color="getLogLevelColor(selectedLog.level)"
size="small"
variant="tonal"
>
{{ selectedLog.level.toUpperCase() }}
</v-chip>
</div>
<div class="mb-3">
<div class="text-caption text-grey-darken-1">SERVICE</div>
<div>{{ selectedLog.service }}</div>
</div>
<div class="mb-3">
<div class="text-caption text-grey-darken-1">MESSAGE</div>
<div>{{ selectedLog.message }}</div>
</div>
<div v-if="selectedLog.stack" class="mb-3">
<div class="text-caption text-grey-darken-1">STACK TRACE</div>
<pre class="text-caption bg-grey-lighten-4 pa-2 rounded overflow-auto" style="max-height: 200px;">{{ selectedLog.stack }}</pre>
</div>
<div v-if="selectedLog.metadata" class="mb-3">
<div class="text-caption text-grey-darken-1">METADATA</div>
<pre class="text-caption bg-grey-lighten-4 pa-2 rounded overflow-auto" style="max-height: 200px;">{{ JSON.stringify(selectedLog.metadata, null, 2) }}</pre>
</div>
</div>
<div v-else class="text-center text-grey-darken-1 py-8">
<v-icon size="48" class="mb-2">mdi-text-box-search</v-icon>
<div>Select a log entry to view details</div>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</div>
</template>
<script setup>
import { formatTime, formatDateTime } from '@/utils/dateUtils'
definePageMeta({
middleware: ['authentication', 'authorization'],
auth: {
roles: ['admin']
}
})
useHead({
title: 'System Logs - Admin'
})
const { isAdmin } = useAuthorization()
// Redirect if not admin
if (!isAdmin()) {
throw createError({
statusCode: 403,
statusMessage: 'Access denied'
})
}
const loading = ref(false)
const autoRefresh = ref(true)
const search = ref('')
const filterLevel = ref('all')
const filterService = ref('all')
const selectedLog = ref(null)
const systemHealth = ref({
status: 'Healthy',
memoryUsage: 45,
cpuUsage: 23,
uptime: '2d 14h 32m'
})
const logs = ref([])
let refreshInterval = null
const filteredLogs = computed(() => {
let filtered = logs.value
if (search.value) {
const searchLower = search.value.toLowerCase()
filtered = filtered.filter(log =>
log.message.toLowerCase().includes(searchLower) ||
log.service.toLowerCase().includes(searchLower)
)
}
if (filterLevel.value !== 'all') {
filtered = filtered.filter(log => log.level === filterLevel.value)
}
if (filterService.value !== 'all') {
filtered = filtered.filter(log => log.service === filterService.value)
}
return filtered.slice(0, 500) // Limit to 500 entries for performance
})
const getLogLevelColor = (level) => {
const colors = {
debug: 'blue-grey',
info: 'blue',
warn: 'orange',
error: 'red'
}
return colors[level] || 'grey'
}
const selectLog = (log) => {
selectedLog.value = log
}
const generateSampleLogs = () => {
const levels = ['debug', 'info', 'warn', 'error']
const services = ['auth', 'api', 'database', 'email', 'storage']
const messages = [
'Authentication successful for user',
'Database query executed successfully',
'Email sent to user',
'File uploaded to storage',
'API request processed',
'Cache miss for key',
'Session expired for user',
'Rate limit exceeded',
'Database connection established',
'Background job completed'
]
const sampleLogs = []
for (let i = 0; i < 100; i++) {
const level = levels[Math.floor(Math.random() * levels.length)]
const service = services[Math.floor(Math.random() * services.length)]
const message = messages[Math.floor(Math.random() * messages.length)]
sampleLogs.push({
id: i + 1,
timestamp: new Date(Date.now() - Math.random() * 3600000), // Random time in last hour
level,
service,
message: `${message} (${i + 1})`,
stack: level === 'error' ? 'Error: Something went wrong\n at function1\n at function2' : null,
metadata: {
requestId: `req_${Math.random().toString(36).substring(2, 9)}`,
duration: Math.floor(Math.random() * 1000) + 'ms'
}
})
}
return sampleLogs.sort((a, b) => b.timestamp - a.timestamp)
}
const refreshLogs = async () => {
loading.value = true
try {
// In a real implementation, this would fetch from your logging service
// For now, we'll generate sample data
await new Promise(resolve => setTimeout(resolve, 500)) // Simulate API call
logs.value = generateSampleLogs()
// Update system health
systemHealth.value = {
status: 'Healthy',
memoryUsage: Math.floor(Math.random() * 30) + 40,
cpuUsage: Math.floor(Math.random() * 20) + 15,
uptime: '2d 14h 32m'
}
} catch (error) {
console.error('Failed to load system logs:', error)
}
loading.value = false
}
const startAutoRefresh = () => {
if (refreshInterval) {
clearInterval(refreshInterval)
}
if (autoRefresh.value) {
refreshInterval = setInterval(() => {
refreshLogs()
}, 5000) // Refresh every 5 seconds
}
}
const stopAutoRefresh = () => {
if (refreshInterval) {
clearInterval(refreshInterval)
refreshInterval = null
}
}
// Watch auto-refresh setting
watch(autoRefresh, (newValue) => {
if (newValue) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
})
// Load initial data
onMounted(() => {
refreshLogs()
startAutoRefresh()
})
// Cleanup on unmount
onBeforeUnmount(() => {
stopAutoRefresh()
})
</script>
<style scoped>
.logs-container {
border: 1px solid #e0e0e0;
border-radius: 4px;
font-family: 'Courier New', monospace;
}
.log-entry {
padding: 8px 12px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.2s;
}
.log-entry:hover {
background-color: #f8f9fa;
}
.log-entry.log-selected {
background-color: #e3f2fd;
border-left: 3px solid #2196f3;
}
.log-debug {
border-left: 3px solid #607d8b;
}
.log-info {
border-left: 3px solid #2196f3;
}
.log-warn {
border-left: 3px solid #ff9800;
}
.log-error {
border-left: 3px solid #f44336;
}
.log-entry:last-child {
border-bottom: none;
}
</style>

View File

@ -78,9 +78,14 @@ export default defineNuxtPlugin(() => {
const checkAndScheduleRefresh = async () => {
try {
const sessionData = await $fetch<{ user: any; authenticated: boolean }>('/api/auth/session')
// Use middleware cache instead of API call
const nuxtApp = useNuxtApp()
const authState = nuxtApp.payload?.data?.authState
const sessionCache = nuxtApp.payload?.data?.['auth:session:cache']
if (sessionData.authenticated) {
const sessionData = authState || sessionCache
if (sessionData?.authenticated) {
// Get the session cookie to extract expiry time
const sessionCookie = useCookie('nuxt-oidc-auth')
@ -98,6 +103,8 @@ export default defineNuxtPlugin(() => {
console.error('[AUTH_REFRESH] Failed to parse session cookie:', parseError)
}
}
} else {
console.log('[AUTH_REFRESH] No authenticated session found in cache')
}
} catch (error) {
console.error('[AUTH_REFRESH] Failed to check session:', error)