393 lines
11 KiB
Vue
393 lines
11 KiB
Vue
<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>
|