From d2adc68050ef53d89c84c5ef4edd4a39e655edf0 Mon Sep 17 00:00:00 2001 From: Remi <73385395+itsrubberduck@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:52:34 +0200 Subject: [PATCH] Add admin transmission log endpoint --- .gitignore | 2 + server/api/admin/logs/transmissions.get.ts | 122 +++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 server/api/admin/logs/transmissions.get.ts diff --git a/.gitignore b/.gitignore index 371d6a7..6f8900d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ node_modules # Logs logs *.log +!server/api/admin/logs +!server/api/admin/logs/** # Misc .DS_Store diff --git a/server/api/admin/logs/transmissions.get.ts b/server/api/admin/logs/transmissions.get.ts new file mode 100644 index 0000000..a7c0ffb --- /dev/null +++ b/server/api/admin/logs/transmissions.get.ts @@ -0,0 +1,122 @@ +import { createError, defineEventHandler, getQuery } from 'h3' +import type { FilterQuery } from 'mongoose' +import { requireAdmin } from '../../../utils/auth' +import { TransmissionLog, type TransmissionLogDocument } from '../../../models/TransmissionLog' + +const CHANNELS = new Set(['ptt', 'say', 'text']) +const DIRECTIONS = new Set(['incoming', 'outgoing']) +const ROLES = new Set(['pilot', 'atc']) + +type TransmissionListItem = { + id: string + role: string + channel: string + direction: string + text: string + normalized?: string + createdAt: string + metadata?: Record + user?: { id: string; email: string; name?: string; role: string } +} + +function escapeRegExp(input: string) { + return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +function mapTransmission(doc: any): TransmissionListItem { + return { + id: String(doc._id), + role: doc.role, + channel: doc.channel, + direction: doc.direction, + text: doc.text, + normalized: doc.normalized || undefined, + createdAt: doc.createdAt ? new Date(doc.createdAt).toISOString() : new Date().toISOString(), + metadata: doc.metadata || undefined, + user: doc.user + ? { + id: String(doc.user._id), + email: doc.user.email, + name: doc.user.name || undefined, + role: doc.user.role, + } + : undefined, + } +} + +export default defineEventHandler(async (event) => { + await requireAdmin(event) + + const query = getQuery(event) + const search = typeof query.search === 'string' ? query.search.trim() : '' + const channel = typeof query.channel === 'string' ? query.channel.trim() : '' + const direction = typeof query.direction === 'string' ? query.direction.trim() : '' + const role = typeof query.role === 'string' ? query.role.trim() : '' + const sinceRaw = typeof query.since === 'string' ? query.since.trim() : '' + + const page = Number.parseInt(String(query.page ?? '1'), 10) || 1 + const pageSizeRaw = Number.parseInt(String(query.pageSize ?? query.limit ?? '20'), 10) + const pageSize = Math.min(Math.max(pageSizeRaw || 20, 1), 100) + const skip = (page - 1) * pageSize + + const filter: FilterQuery = {} + const andConditions: FilterQuery[] = [] + + if (search) { + const regex = new RegExp(escapeRegExp(search), 'i') + andConditions.push({ $or: [{ text: regex }, { normalized: regex }] }) + } + + if (channel) { + if (!CHANNELS.has(channel)) { + throw createError({ statusCode: 400, statusMessage: 'Unbekannter Kanal' }) + } + filter.channel = channel as TransmissionLogDocument['channel'] + } + + if (direction) { + if (!DIRECTIONS.has(direction)) { + throw createError({ statusCode: 400, statusMessage: 'Ungültige Richtung' }) + } + filter.direction = direction as TransmissionLogDocument['direction'] + } + + if (role) { + if (!ROLES.has(role)) { + throw createError({ statusCode: 400, statusMessage: 'Unbekannte Rolle' }) + } + filter.role = role + } + + if (sinceRaw) { + const since = new Date(sinceRaw) + if (Number.isNaN(since.valueOf())) { + throw createError({ statusCode: 400, statusMessage: 'Ungültiger Zeitraum' }) + } + filter.createdAt = { ...(filter.createdAt as any), $gte: since } + } + + if (andConditions.length) { + filter.$and = andConditions + } + + const [total, items] = await Promise.all([ + TransmissionLog.countDocuments(filter), + TransmissionLog.find(filter) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(pageSize) + .populate('user', 'email name role') + .lean(), + ]) + + return { + items: items.map(mapTransmission), + pagination: { + total, + page, + pageSize, + pages: Math.ceil(total / pageSize) || 1, + }, + } +})