mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-05-13 01:46:08 +08:00
Unify bridge auth header and add live telemetry panel
This commit is contained in:
@@ -112,6 +112,10 @@
|
||||
protected routes via <code class="bg-white/10 px-1">Authorization: Bearer <token></code>. Refresh the token by
|
||||
calling <code class="bg-white/10 px-1">POST /api/service/auth/refresh</code> with the refresh cookie present.
|
||||
</p>
|
||||
<p>
|
||||
Bridge endpoints under <code class="bg-white/10 px-1">/api/bridge/*</code> use
|
||||
<code class="bg-white/10 px-1">x-bridge-token</code> instead of the Authorization header for bridge authentication.
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-lg font-semibold text-white">Base URLs</h3>
|
||||
@@ -128,7 +132,7 @@
|
||||
<h2 class="text-2xl font-semibold">Endpoint catalog</h2>
|
||||
<p class="text-sm text-white/60">
|
||||
Endpoints are grouped by audience. Public endpoints do not require a bearer token. Protected endpoints require a
|
||||
valid access token. Rate limits and additional business rules are documented per route.
|
||||
valid access token. Bridge endpoints require <code class="bg-white/10 px-1">x-bridge-token</code>. Rate limits and additional business rules are documented per route.
|
||||
</p>
|
||||
</header>
|
||||
<p v-if="hasActiveSearch && filteredSections.length" class="text-sm text-white/50">
|
||||
@@ -201,6 +205,9 @@
|
||||
class="inline-flex items-center gap-2 rounded-full border border-white/20 px-3 py-1">
|
||||
<v-icon icon="mdi-lock" size="14" /> Access token required
|
||||
</span>
|
||||
<span v-else-if="endpoint.auth === 'bridge'" class="inline-flex items-center gap-2 rounded-full border border-white/20 px-3 py-1">
|
||||
<v-icon icon="mdi-connection" size="14" /> Bridge token required
|
||||
</span>
|
||||
<span v-else class="inline-flex items-center gap-2 rounded-full border border-white/20 px-3 py-1">
|
||||
<v-icon icon="mdi-earth" size="14" /> Public
|
||||
</span>
|
||||
@@ -336,7 +343,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
type AuthLevel = 'public' | 'protected'
|
||||
type AuthLevel = 'public' | 'protected' | 'bridge'
|
||||
|
||||
interface QueryField {
|
||||
name: string
|
||||
@@ -408,6 +415,7 @@ const BASE_URL = 'https://opensquawk.de'
|
||||
const sectionCategoryOrder: Record<string, string[]> = {
|
||||
'Public endpoints': ['Waitlist & marketing', 'Feedback & insights', 'Authentication & onboarding', 'Tools & diagnostics'],
|
||||
'Protected endpoints': ['Session controls', 'Invitation management', 'ATC synthesis', 'Decision engine'],
|
||||
'Bridge endpoints': ['Bridge integration'],
|
||||
}
|
||||
|
||||
const sectionCategoryDescriptions: Record<string, Record<string, string>> = {
|
||||
@@ -423,6 +431,9 @@ const sectionCategoryDescriptions: Record<string, Record<string, string>> = {
|
||||
'ATC synthesis': 'Voice generation and transcription endpoints for ATC training scenarios.',
|
||||
'Decision engine': 'LLM router access for orchestrating scenario logic.',
|
||||
},
|
||||
'Bridge endpoints': {
|
||||
'Bridge integration': 'Routes used by the desktop bridge client. Bridge authentication uses the x-bridge-token header.',
|
||||
},
|
||||
}
|
||||
|
||||
const endpointSections: EndpointSection[] = [
|
||||
@@ -1128,6 +1139,107 @@ const endpointSections: EndpointSection[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Bridge endpoints',
|
||||
description: 'Bridge routes authenticate with x-bridge-token. The connect route additionally requires a user access token.',
|
||||
endpoints: [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/bridge/me',
|
||||
summary: 'Get bridge link and simulator status for the provided bridge token.',
|
||||
category: 'Bridge integration',
|
||||
auth: 'bridge',
|
||||
sampleRequest: `curl -X GET https://opensquawk.de/api/bridge/me \
|
||||
-H 'x-bridge-token: <bridge-token>'`,
|
||||
sampleResponse: `{
|
||||
"token": "<bridge-token>",
|
||||
"connected": true,
|
||||
"user": {
|
||||
"id": "661e2a...",
|
||||
"email": "jane.pilot@example.com",
|
||||
"name": "Jane Pilot"
|
||||
},
|
||||
"simConnected": true,
|
||||
"flightActive": false,
|
||||
"connectedAt": "2026-02-15T10:10:10.000Z",
|
||||
"lastStatusAt": "2026-02-15T10:12:00.000Z"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/bridge/status',
|
||||
summary: 'Update heartbeat/sim status for a bridge instance.',
|
||||
category: 'Bridge integration',
|
||||
auth: 'bridge',
|
||||
body: [
|
||||
{ name: 'simConnected', type: 'boolean', description: 'Whether the simulator is currently reachable.' },
|
||||
{ name: 'flightActive', type: 'boolean', description: 'Whether an active flight is currently detected.' },
|
||||
],
|
||||
sampleRequest: `curl -X POST https://opensquawk.de/api/bridge/status \
|
||||
-H 'x-bridge-token: <bridge-token>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"simConnected": true,
|
||||
"flightActive": false
|
||||
}'`,
|
||||
sampleResponse: `{
|
||||
"token": "<bridge-token>",
|
||||
"connected": true,
|
||||
"user": {
|
||||
"id": "661e2a...",
|
||||
"email": "jane.pilot@example.com",
|
||||
"name": "Jane Pilot"
|
||||
},
|
||||
"simConnected": true,
|
||||
"flightActive": false,
|
||||
"connectedAt": "2026-02-15T10:10:10.000Z",
|
||||
"lastStatusAt": "2026-02-15T10:12:00.000Z"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/bridge/data',
|
||||
summary: 'Stream telemetry payloads from the bridge into the FlightLab telemetry channel.',
|
||||
category: 'Bridge integration',
|
||||
auth: 'bridge',
|
||||
body: [
|
||||
{ name: '<telemetry keys>', type: 'object', required: true, description: 'Arbitrary telemetry fields from the sim bridge payload.' },
|
||||
],
|
||||
sampleRequest: `curl -X POST https://opensquawk.de/api/bridge/data \
|
||||
-H 'x-bridge-token: <bridge-token>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"AIRSPEED_INDICATED": 145.2,
|
||||
"GROUND_VELOCITY": 142.8,
|
||||
"VERTICAL_SPEED": 0,
|
||||
"PLANE_ALTITUDE": 364,
|
||||
"SIM_ON_GROUND": true
|
||||
}'`,
|
||||
sampleResponse: `HTTP 204 No Content`,
|
||||
notes: 'Returns HTTP 401 if the bridge token is missing, invalid, or not linked to a user.',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/bridge/connect',
|
||||
summary: 'Link the bridge token to the currently signed-in user account.',
|
||||
category: 'Bridge integration',
|
||||
auth: 'protected',
|
||||
sampleRequest: `curl -X POST https://opensquawk.de/api/bridge/connect \
|
||||
-H 'Authorization: Bearer <token>' \
|
||||
-H 'x-bridge-token: <bridge-token>'`,
|
||||
sampleResponse: `{
|
||||
"success": true,
|
||||
"token": "<bridge-token>",
|
||||
"connectedAt": "2026-02-15T10:10:10.000Z",
|
||||
"user": {
|
||||
"id": "661e2a...",
|
||||
"email": "jane.pilot@example.com",
|
||||
"name": "Jane Pilot"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const searchTerm = ref('')
|
||||
|
||||
@@ -196,6 +196,31 @@
|
||||
<p class="mt-1 font-medium text-white">{{ formattedLastStatusAt }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-white/10 bg-[#0B132A]/75">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-4 py-3 text-left"
|
||||
@click="liveTelemetryOpen = !liveTelemetryOpen"
|
||||
>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-white">Live telemetry details</p>
|
||||
<p class="text-xs text-white/50">Last telemetry: {{ formattedLastTelemetryAt }}</p>
|
||||
</div>
|
||||
<span class="text-xs uppercase tracking-[0.24em] text-white/55">
|
||||
{{ liveTelemetryOpen ? 'Hide' : 'Show' }}
|
||||
</span>
|
||||
</button>
|
||||
<div v-if="liveTelemetryOpen" class="border-t border-white/10 px-4 py-3">
|
||||
<p v-if="liveTelemetryLoading" class="text-xs uppercase tracking-[0.24em] text-white/45">Loading telemetry …</p>
|
||||
<p v-else-if="liveTelemetryError" class="text-sm text-red-300">{{ liveTelemetryError }}</p>
|
||||
<p v-else-if="!liveTelemetry?.telemetry" class="text-sm text-white/60">No telemetry received yet.</p>
|
||||
<pre
|
||||
v-else
|
||||
class="max-h-72 overflow-auto rounded-xl border border-white/10 bg-black/40 p-3 text-xs leading-5 text-[#9be6f2]"
|
||||
><code>{{ liveTelemetryJson }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="statusLoading" class="mt-6 text-xs uppercase tracking-[0.38em] text-white/45">Refreshing …</p>
|
||||
@@ -226,6 +251,12 @@ interface BridgeStatusPayload {
|
||||
lastStatusAt: string | null
|
||||
}
|
||||
|
||||
interface BridgeLivePayload {
|
||||
connected: boolean
|
||||
lastTelemetryAt: string | null
|
||||
telemetry: Record<string, any> | 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<BridgeLivePayload | null>(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<BridgeStatusPayload>('/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<BridgeLivePayload>('/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()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,21 +1,47 @@
|
||||
MSFS Telemetry: { status: 'idle', ts: 1758407137, sim_connected: true, flight_loaded: false } 12:27:11 AM
|
||||
MSFS Telemetry: { status: 'idle', ts: 1758407227, sim_connected: true, flight_loaded: false } 12:27:11 AM
|
||||
MSFS Telemetry: { status: 'idle', ts: 1758407237, sim_connected: true, flight_loaded: false } 12:27:11 AM
|
||||
MSFS Telemetry: { status: 'active', 12:27:14 AM
|
||||
ts: 1758407240,
|
||||
latitude: 50.043941,
|
||||
longitude: 8.581944,
|
||||
altitude_ft_true: 369,
|
||||
altitude_ft_indicated: 365,
|
||||
ias_kt: 12.8,
|
||||
tas_kt: 12.9,
|
||||
groundspeed_kt: 12,
|
||||
on_ground: true,
|
||||
eng_on: true,
|
||||
n1_pct: 19.8,
|
||||
transponder_code: 0,
|
||||
com_active_frequency: 118.000,
|
||||
com_standby_frequency: 118.000,
|
||||
MSFS Telemetry: { status: 'idle', ts: 1758407248, sim_connected: false, flight_loaded: false } 12:27:22 AM
|
||||
MSFS Telemetry: { status: 'idle', ts: 1758407293, sim_connected: false, flight_loaded: false } 12:28:07 AM
|
||||
MSFS Telemetry: { status: 'idle', ts: 1758407338, sim_connected: false, flight_loaded: false }
|
||||
# MSFS Bridge API Examples
|
||||
|
||||
All bridge endpoints authenticate bridge clients via the `x-bridge-token` header.
|
||||
|
||||
## 1. Read Bridge status
|
||||
|
||||
```bash
|
||||
curl -X GET 'https://opensquawk.de/api/bridge/me' \
|
||||
-H 'x-bridge-token: <bridge-token>'
|
||||
```
|
||||
|
||||
## 2. Push Bridge heartbeat/status
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://opensquawk.de/api/bridge/status' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'x-bridge-token: <bridge-token>' \
|
||||
-d '{
|
||||
"simConnected": true,
|
||||
"flightActive": false
|
||||
}'
|
||||
```
|
||||
|
||||
## 3. Push telemetry data
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://opensquawk.de/api/bridge/data' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'x-bridge-token: <bridge-token>' \
|
||||
-d '{
|
||||
"AIRSPEED_INDICATED": 145.2,
|
||||
"GROUND_VELOCITY": 142.8,
|
||||
"VERTICAL_SPEED": 0,
|
||||
"PLANE_ALTITUDE": 364,
|
||||
"SIM_ON_GROUND": true
|
||||
}'
|
||||
```
|
||||
|
||||
## 4. Link bridge token to signed-in user
|
||||
|
||||
This endpoint still requires a user JWT for account auth, plus the bridge token header:
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://opensquawk.de/api/bridge/connect' \
|
||||
-H 'Authorization: Bearer <user-jwt>' \
|
||||
-H 'x-bridge-token: <bridge-token>'
|
||||
```
|
||||
|
||||
@@ -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<ConnectBody>(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) => {
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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 <user-jwt-token>
|
||||
* x-bridge-token: <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<string, unknown>) : []
|
||||
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)
|
||||
|
||||
33
server/api/bridge/live.get.ts
Normal file
33
server/api/bridge/live.get.ts
Normal file
@@ -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,
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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<StatusBody>(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<StatusBody>(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<string, unknown> = {
|
||||
lastStatusAt: new Date(),
|
||||
}
|
||||
@@ -60,4 +62,3 @@ export default defineEventHandler(async (event) => {
|
||||
lastStatusAt: document.lastStatusAt ? document.lastStatusAt.toISOString() : null,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user