Harden audit privacy and admin modals
This commit is contained in:
@@ -128,9 +128,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, watch } from 'vue'
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import { Braces, ExternalLink, GitCompareArrows, ListTree, ShieldCheck, X } from '@lucide/vue'
|
||||
|
||||
import { useBodyScrollLock } from '../../composables/useBodyScrollLock'
|
||||
import type { AuditLogRow } from './useAdminAuditManager'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -145,18 +146,10 @@ function closeOnEscape(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && props.entry) emit('close')
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.entry,
|
||||
(entry) => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.body.style.overflow = entry ? 'hidden' : ''
|
||||
}
|
||||
},
|
||||
)
|
||||
useBodyScrollLock(() => Boolean(props.entry))
|
||||
|
||||
onMounted(() => window.addEventListener('keydown', closeOnEscape))
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', closeOnEscape)
|
||||
if (typeof document !== 'undefined') document.body.style.overflow = ''
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
import { X } from '@lucide/vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useBodyScrollLock } from '../../composables/useBodyScrollLock'
|
||||
import type { FaqFormItem } from './adminContentTypes'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -53,6 +54,8 @@ const props = defineProps<{
|
||||
faq: FaqFormItem[]
|
||||
}>()
|
||||
|
||||
useBodyScrollLock(() => props.open)
|
||||
|
||||
defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
@@ -39,13 +39,17 @@
|
||||
<script setup lang="ts">
|
||||
import { X } from '@lucide/vue'
|
||||
|
||||
defineProps<{
|
||||
import { useBodyScrollLock } from '../../composables/useBodyScrollLock'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
title: string
|
||||
url: string
|
||||
contentHtml: string
|
||||
}>()
|
||||
|
||||
useBodyScrollLock(() => props.open)
|
||||
|
||||
defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
@@ -39,12 +39,16 @@
|
||||
<script setup lang="ts">
|
||||
import { X } from '@lucide/vue'
|
||||
|
||||
defineProps<{
|
||||
import { useBodyScrollLock } from '../../composables/useBodyScrollLock'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
contentHtml: string
|
||||
updatedLabel: string
|
||||
}>()
|
||||
|
||||
useBodyScrollLock(() => props.open)
|
||||
|
||||
defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, watch } from 'vue'
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import { Sparkles, X } from '@lucide/vue'
|
||||
|
||||
import AdminReviewDecisionPanel from './AdminReviewDecisionPanel.vue'
|
||||
@@ -7,6 +7,7 @@ import AdminReviewsHistorySection from './AdminReviewsHistorySection.vue'
|
||||
import AdminReviewsQueueHeader from './AdminReviewsQueueHeader.vue'
|
||||
import AdminReviewsQueueList from './AdminReviewsQueueList.vue'
|
||||
import { useAdminReviewsManager } from './useAdminReviewsManager'
|
||||
import { useBodyScrollLock } from '../../composables/useBodyScrollLock'
|
||||
import { watchAdminToast } from '../../composables/useAdminToast'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -55,20 +56,11 @@ function onKey(event: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.body.style.overflow = open ? 'hidden' : ''
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
useBodyScrollLock(() => props.open)
|
||||
|
||||
onMounted(() => window.addEventListener('keydown', onKey))
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', onKey)
|
||||
if (typeof document !== 'undefined') document.body.style.overflow = ''
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useAwardsStore } from '../../stores/awards'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import type { AdminAuditEntry, AdminAuditQueryOptions } from '../../types/awards'
|
||||
|
||||
const auditPageLimit = 80
|
||||
@@ -294,9 +295,9 @@ function getDateBoundary(value: string, isEndOfDay: boolean) {
|
||||
return Number.isNaN(date.getTime()) ? undefined : date.toISOString()
|
||||
}
|
||||
|
||||
function buildRequestContextItems(entry: AdminAuditEntry) {
|
||||
function buildRequestContextItems(entry: AdminAuditEntry, canViewAuditIp: boolean) {
|
||||
return [
|
||||
{ key: 'IP', value: entry.createdFromIp || 'nicht erfasst' },
|
||||
...(canViewAuditIp ? [{ key: 'IP', value: entry.createdFromIp || 'nicht erfasst' }] : []),
|
||||
{ key: 'User-Agent', value: entry.userAgent || 'nicht erfasst' },
|
||||
]
|
||||
}
|
||||
@@ -312,7 +313,7 @@ function buildRelatedLink(entry: AdminAuditEntry) {
|
||||
return route
|
||||
}
|
||||
|
||||
function createAuditRow(entry: AdminAuditEntry): AuditLogRow {
|
||||
function createAuditRow(entry: AdminAuditEntry, canViewAuditIp: boolean): AuditLogRow {
|
||||
return {
|
||||
...entry,
|
||||
actionLabel: humanizeAction(entry.actionType),
|
||||
@@ -324,13 +325,13 @@ function createAuditRow(entry: AdminAuditEntry): AuditLogRow {
|
||||
ageLabel: formatAge(entry.createdAt),
|
||||
metadataItems: parseMetadata(entry.metadataJson),
|
||||
changeItems: parseChangeItems(entry.metadataJson),
|
||||
requestContextItems: buildRequestContextItems(entry),
|
||||
requestContextItems: buildRequestContextItems(entry, canViewAuditIp),
|
||||
relatedLink: buildRelatedLink(entry),
|
||||
rawMetadataJson: entry.metadataJson || '{}',
|
||||
}
|
||||
}
|
||||
|
||||
function downloadCsv(entries: AdminAuditEntry[]) {
|
||||
function downloadCsv(entries: AdminAuditEntry[], canViewAuditIp: boolean) {
|
||||
const rows = [
|
||||
[
|
||||
'Id',
|
||||
@@ -340,7 +341,7 @@ function downloadCsv(entries: AdminAuditEntry[]) {
|
||||
'Objekt-Id',
|
||||
'Zusammenfassung',
|
||||
'Zeitpunkt',
|
||||
'IP',
|
||||
...(canViewAuditIp ? ['IP'] : []),
|
||||
'User-Agent',
|
||||
'Metadaten',
|
||||
],
|
||||
@@ -352,7 +353,7 @@ function downloadCsv(entries: AdminAuditEntry[]) {
|
||||
entry.entityId,
|
||||
entry.summary,
|
||||
new Date(entry.createdAt).toISOString(),
|
||||
entry.createdFromIp,
|
||||
...(canViewAuditIp ? [entry.createdFromIp ?? ''] : []),
|
||||
entry.userAgent,
|
||||
entry.metadataJson,
|
||||
]),
|
||||
@@ -372,6 +373,7 @@ function downloadCsv(entries: AdminAuditEntry[]) {
|
||||
|
||||
export function useAdminAuditManager() {
|
||||
const store = useAwardsStore()
|
||||
const authStore = useAuthStore()
|
||||
const query = ref('')
|
||||
const selectedAdmin = ref(allFilter)
|
||||
const selectedAction = ref(allFilter)
|
||||
@@ -405,9 +407,10 @@ export function useAdminAuditManager() {
|
||||
label: entityOptions.find((option) => option.value === item.key)?.label ?? item.key,
|
||||
})),
|
||||
)
|
||||
const auditRows = computed<AuditLogRow[]>(() => auditEntries.value.map(createAuditRow))
|
||||
const canViewAuditIp = computed(() => authStore.isOwnerOrCreator)
|
||||
const auditRows = computed<AuditLogRow[]>(() => auditEntries.value.map((entry) => createAuditRow(entry, canViewAuditIp.value)))
|
||||
const metadataCount = computed(() => auditEntries.value.filter((entry) => parseMetadata(entry.metadataJson).length > 0).length)
|
||||
const requestContextCount = computed(() => auditEntries.value.filter((entry) => entry.createdFromIp || entry.userAgent).length)
|
||||
const requestContextCount = computed(() => auditEntries.value.filter((entry) => entry.userAgent || (canViewAuditIp.value && entry.createdFromIp)).length)
|
||||
const recentDayCount = computed(() => {
|
||||
const minTimestamp = Date.now() - 24 * 60 * 60 * 1000
|
||||
return auditEntries.value.filter((entry) => new Date(entry.createdAt).getTime() >= minTimestamp).length
|
||||
@@ -419,7 +422,7 @@ export function useAdminAuditManager() {
|
||||
{ label: 'Geladen', value: loadedCountLabel.value, note: `Page ${auditPageLimit}` },
|
||||
{ label: 'Treffer', value: totalCountLabel.value, note: 'serverseitig gefiltert' },
|
||||
{ label: '24h', value: recentDayCount.value.toLocaleString('de-DE'), note: 'neue Aktionen' },
|
||||
{ label: 'Kontext', value: requestContextCount.value.toLocaleString('de-DE'), note: 'mit IP oder User-Agent' },
|
||||
{ label: 'Kontext', value: requestContextCount.value.toLocaleString('de-DE'), note: canViewAuditIp.value ? 'mit IP oder User-Agent' : 'mit User-Agent' },
|
||||
])
|
||||
const focusCards = computed<AuditFocusCard[]>(() => {
|
||||
const topAdmin = adminCounts.value[0]
|
||||
@@ -532,7 +535,7 @@ export function useAdminAuditManager() {
|
||||
|
||||
function exportAuditCsv() {
|
||||
if (auditEntries.value.length === 0) return
|
||||
downloadCsv(auditEntries.value)
|
||||
downloadCsv(auditEntries.value, canViewAuditIp.value)
|
||||
exportMessage.value = `CSV mit ${auditEntries.value.length.toLocaleString('de-DE')} geladenen Einträgen erstellt.`
|
||||
if (exportMessageTimer) window.clearTimeout(exportMessageTimer)
|
||||
exportMessageTimer = window.setTimeout(() => {
|
||||
|
||||
Reference in New Issue
Block a user