diff --git a/app/pages/bridge/connect.vue b/app/pages/bridge/connect.vue index 6d86d79..32f3e26 100644 --- a/app/pages/bridge/connect.vue +++ b/app/pages/bridge/connect.vue @@ -196,6 +196,31 @@

{{ formattedLastStatusAt }}

+ +
+ +
+

Loading telemetry …

+

{{ liveTelemetryError }}

+

No telemetry received yet.

+
{{ liveTelemetryJson }}
+
+

Refreshing …

@@ -226,6 +251,12 @@ interface BridgeStatusPayload { lastStatusAt: string | null } +interface BridgeLivePayload { + connected: boolean + lastTelemetryAt: string | null + telemetry: Record | null +} + useHead({ title: 'Link Bridge · OpenSquawk' }) const route = useRoute() @@ -241,6 +272,11 @@ const statusInitialized = ref(false) const statusError = ref('') const statusLoading = ref(false) const statusRequestActive = ref(false) +const liveTelemetry = ref(null) +const liveTelemetryOpen = ref(false) +const liveTelemetryLoading = ref(false) +const liveTelemetryError = ref('') +const liveTelemetryRequestActive = ref(false) const copiedToken = ref(false) @@ -277,6 +313,13 @@ const connectionSubLabel = computed(() => { const formattedConnectedAt = computed(() => formatTimestamp(connectionStatus.value?.connectedAt ?? null)) const formattedLastStatusAt = computed(() => formatTimestamp(connectionStatus.value?.lastStatusAt ?? null)) +const formattedLastTelemetryAt = computed(() => formatTimestamp(liveTelemetry.value?.lastTelemetryAt ?? null)) +const liveTelemetryJson = computed(() => { + if (!liveTelemetry.value?.telemetry) { + return null + } + return JSON.stringify(liveTelemetry.value.telemetry, null, 2) +}) const successBannerVisible = computed(() => connectSuccess.value || Boolean(connectionStatus.value?.connected)) @@ -304,9 +347,9 @@ async function connectBridge() { try { await $fetch('/api/bridge/connect', { method: 'POST', - body: { token: token.value }, headers: { Authorization: `Bearer ${accessToken.value}`, + 'x-bridge-token': token.value, }, }) connectSuccess.value = true @@ -337,7 +380,9 @@ async function fetchStatus(force = false) { try { const response = await $fetch('/api/bridge/me', { - params: { token: token.value }, + headers: { + 'x-bridge-token': token.value, + }, }) connectionStatus.value = response statusError.value = '' @@ -345,6 +390,7 @@ async function fetchStatus(force = false) { if (response.connected) { connectSuccess.value = true } + await fetchLiveTelemetry(force) } catch (err: any) { statusError.value = err?.data?.statusMessage || @@ -357,6 +403,39 @@ async function fetchStatus(force = false) { } } +async function fetchLiveTelemetry(force = false) { + if (!hasToken.value) { + return + } + if (liveTelemetryRequestActive.value) { + return + } + + liveTelemetryRequestActive.value = true + if (force || !liveTelemetry.value) { + liveTelemetryLoading.value = true + } + + try { + const response = await $fetch('/api/bridge/live', { + headers: { + 'x-bridge-token': token.value, + }, + }) + liveTelemetry.value = response + liveTelemetryError.value = '' + } catch (err: any) { + liveTelemetryError.value = + err?.data?.statusMessage || + err?.response?._data?.statusMessage || + err?.message || + 'We could not fetch live telemetry.' + } finally { + liveTelemetryRequestActive.value = false + liveTelemetryLoading.value = false + } +} + async function copyToken() { if (!hasToken.value) return if (typeof navigator === 'undefined' || !navigator.clipboard) return @@ -397,6 +476,9 @@ watch(token, () => { connectionStatus.value = null statusInitialized.value = false statusError.value = '' + liveTelemetry.value = null + liveTelemetryError.value = '' + liveTelemetryOpen.value = false startPolling() }) diff --git a/server/api/bridge/connect.post.ts b/server/api/bridge/connect.post.ts index dfb2b5e..2535d47 100644 --- a/server/api/bridge/connect.post.ts +++ b/server/api/bridge/connect.post.ts @@ -1,21 +1,20 @@ -import { createError, readBody } from 'h3' +import { createError } from 'h3' import { requireUserSession } from '../../utils/auth' -import { normalizeBridgeToken } from '../../utils/bridge' +import { getBridgeTokenFromHeader } from '../../utils/bridge' import { BridgeToken } from '../../models/BridgeToken' -interface ConnectBody { - token?: string -} - export default defineEventHandler(async (event) => { const user = await requireUserSession(event) - const body = await readBody(event) - const token = normalizeBridgeToken(body.token) + const token = getBridgeTokenFromHeader(event) if (!token) { - throw createError({ statusCode: 400, statusMessage: 'Ungültiger Token übergeben.' }) + throw createError({ statusCode: 401, statusMessage: 'x-bridge-token header fehlt oder ist ungültig.' }) } + console.info( + `\x1b[32m[bridge:connect]\x1b[0m token=\x1b[96m${token.slice(0, 6)}...\x1b[0m user=\x1b[92m${String(user._id)}\x1b[0m`, + ) + const now = new Date() const document = await BridgeToken.findOneAndUpdate( @@ -47,4 +46,3 @@ export default defineEventHandler(async (event) => { }, } }) - diff --git a/server/api/bridge/data.post.ts b/server/api/bridge/data.post.ts index 8e32c49..f538e1b 100644 --- a/server/api/bridge/data.post.ts +++ b/server/api/bridge/data.post.ts @@ -1,11 +1,12 @@ -import { defineEventHandler, readBody, getHeader } from 'h3' -import { resolveUserFromToken } from '../../utils/auth' +import { createError, defineEventHandler, readBody } from 'h3' +import { BridgeToken } from '../../models/BridgeToken' +import { getBridgeTokenFromHeader } from '../../utils/bridge' import { flightlabTelemetryStore } from '../../utils/flightlabTelemetry' /** * Receives MSFS SimConnect telemetry data from an external bridge application. * - * The bridge should POST telemetry data with an Authorization header so we + * The bridge should POST telemetry data with an x-bridge-token header so we * can route the data to the correct FlightLab WebSocket session. * * ────────────────────────────────────────── @@ -13,7 +14,7 @@ import { flightlabTelemetryStore } from '../../utils/flightlabTelemetry' * ────────────────────────────────────────── * * POST /api/bridge/data - * Authorization: Bearer + * x-bridge-token: * Content-Type: application/json * * { @@ -37,17 +38,23 @@ import { flightlabTelemetryStore } from '../../utils/flightlabTelemetry' * ────────────────────────────────────────── */ export default defineEventHandler(async (event) => { - // Resolve user from Bearer token (optional — also works with ?userId query param) - const user = await resolveUserFromToken(event) - const userId = user?._id?.toString() - ?? new URL(event.node.req.url ?? '', 'http://localhost').searchParams.get('userId') + const bridgeToken = getBridgeTokenFromHeader(event) + if (!bridgeToken) { + throw createError({ statusCode: 401, statusMessage: 'x-bridge-token header fehlt oder ist ungültig.' }) + } + const bridgeDocument = await BridgeToken.findOne({ token: bridgeToken }).select('user') + const userId = bridgeDocument?.user?.toString() ?? null if (!userId) { - event.node.res.statusCode = 401 - return { error: 'Authorization required — send Bearer token or ?userId query param' } + throw createError({ statusCode: 401, statusMessage: 'Bridge-Token ist nicht mit einem Nutzer verknüpft.' }) } const body = await readBody(event) + const telemetryKeys = body && typeof body === 'object' ? Object.keys(body as Record) : [] + console.info( + `\x1b[35m[bridge:data]\x1b[0m token=\x1b[96m${bridgeToken.slice(0, 6)}...\x1b[0m user=\x1b[92m${userId}\x1b[0m telemetryKeys=\x1b[92m${telemetryKeys.length}\x1b[0m payload=`, + body, + ) // Store telemetry and broadcast to WebSocket subscribers flightlabTelemetryStore.update(userId, body) diff --git a/server/api/bridge/live.get.ts b/server/api/bridge/live.get.ts new file mode 100644 index 0000000..744f878 --- /dev/null +++ b/server/api/bridge/live.get.ts @@ -0,0 +1,33 @@ +import { createError } from 'h3' +import { BridgeToken } from '../../models/BridgeToken' +import { getBridgeTokenFromHeader } from '../../utils/bridge' +import { flightlabTelemetryStore } from '../../utils/flightlabTelemetry' + +export default defineEventHandler(async (event) => { + const token = getBridgeTokenFromHeader(event) + if (!token) { + throw createError({ statusCode: 401, statusMessage: 'x-bridge-token header fehlt oder ist ungültig.' }) + } + + const bridgeDocument = await BridgeToken.findOne({ token }).select('user') + const userId = bridgeDocument?.user?.toString() ?? null + + if (!userId) { + return { + connected: false, + lastTelemetryAt: null, + telemetry: null, + } + } + + const telemetry = flightlabTelemetryStore.get(userId) + const timestamp = telemetry && typeof telemetry.timestamp === 'number' + ? new Date(telemetry.timestamp).toISOString() + : null + + return { + connected: true, + lastTelemetryAt: timestamp, + telemetry, + } +}) diff --git a/server/api/bridge/me.get.ts b/server/api/bridge/me.get.ts index ee36616..b8d6c13 100644 --- a/server/api/bridge/me.get.ts +++ b/server/api/bridge/me.get.ts @@ -1,16 +1,17 @@ -import { createError, getQuery } from 'h3' +import { createError } from 'h3' import { BridgeToken } from '../../models/BridgeToken' -import { normalizeBridgeToken } from '../../utils/bridge' +import { getBridgeTokenFromHeader } from '../../utils/bridge' import type { UserDocument } from '../../models/User' export default defineEventHandler(async (event) => { - const query = getQuery(event) - const token = normalizeBridgeToken(query.token as string | undefined) + const token = getBridgeTokenFromHeader(event) if (!token) { - throw createError({ statusCode: 400, statusMessage: 'Token muss übergeben werden.' }) + throw createError({ statusCode: 401, statusMessage: 'x-bridge-token header fehlt oder ist ungültig.' }) } + console.info(`\x1b[36m[bridge:me]\x1b[0m token=\x1b[96m${token.slice(0, 6)}...\x1b[0m request received`) + const document = await BridgeToken.findOne({ token }).populate('user', 'name email') if (!document || !document.user) { @@ -41,4 +42,3 @@ export default defineEventHandler(async (event) => { lastStatusAt: document.lastStatusAt ? document.lastStatusAt.toISOString() : null, } }) - diff --git a/server/api/bridge/status.post.ts b/server/api/bridge/status.post.ts index 89c8f1d..436bf7c 100644 --- a/server/api/bridge/status.post.ts +++ b/server/api/bridge/status.post.ts @@ -1,22 +1,24 @@ import { createError, readBody } from 'h3' import { BridgeToken } from '../../models/BridgeToken' -import { normalizeBridgeToken } from '../../utils/bridge' +import { getBridgeTokenFromHeader } from '../../utils/bridge' import type { UserDocument } from '../../models/User' interface StatusBody { - token?: string simConnected?: boolean flightActive?: boolean } export default defineEventHandler(async (event) => { - const body = await readBody(event) - const token = normalizeBridgeToken(body.token) - + const token = getBridgeTokenFromHeader(event) if (!token) { - throw createError({ statusCode: 400, statusMessage: 'Token muss übergeben werden.' }) + throw createError({ statusCode: 401, statusMessage: 'x-bridge-token header fehlt oder ist ungültig.' }) } + const body = await readBody(event) + console.info( + `\x1b[33m[bridge:status]\x1b[0m token=\x1b[96m${token.slice(0, 6)}...\x1b[0m simConnected=\x1b[92m${String(body.simConnected)}\x1b[0m flightActive=\x1b[92m${String(body.flightActive)}\x1b[0m`, + ) + const updatePayload: Record = { lastStatusAt: new Date(), } @@ -60,4 +62,3 @@ export default defineEventHandler(async (event) => { lastStatusAt: document.lastStatusAt ? document.lastStatusAt.toISOString() : null, } }) - diff --git a/server/utils/bridge.ts b/server/utils/bridge.ts index 4b9ab33..cd1da3b 100644 --- a/server/utils/bridge.ts +++ b/server/utils/bridge.ts @@ -1,3 +1,5 @@ +import { getHeader, type H3Event } from 'h3' + export function normalizeBridgeToken(input: unknown) { if (typeof input !== 'string') { return null @@ -12,3 +14,6 @@ export function normalizeBridgeToken(input: unknown) { return token } +export function getBridgeTokenFromHeader(event: H3Event) { + return normalizeBridgeToken(getHeader(event, 'x-bridge-token')) +}