fix(atc): use authenticated API calls for Live ATC

- Revert auth middleware whitelist: /api/atc/route and /api/atc/ptt
  require authentication again (only /api/atc/say stays open)
- liveatc.vue: add require-auth middleware, use useApi() for all API calls
- engine.ts: accept apiFetch option to use authenticated fetch function
  instead of bare $fetch, so Bearer token is sent with all requests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-02-15 00:27:14 +01:00
parent cd272e2b6c
commit a2fbdae511
3 changed files with 34 additions and 20 deletions

View File

@@ -183,11 +183,27 @@ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { useAtcEngine } from '~~/shared/atc/engine'
import { getPhaseOrder, getPhase } from '~~/shared/atc/phases'
import type { Transmission, FlightPlan } from '~~/shared/atc/types'
import { useApi } from '~/composables/useApi'
import { useAuthStore } from '~/stores/auth'
definePageMeta({ middleware: ['require-auth'] })
// For radio effects (conditional import)
let applyRadioEffect: ((audioBuffer: AudioBuffer, ctx: AudioContext, level: number) => AudioBuffer) | null = null
const engine = useAtcEngine()
const api = useApi()
const auth = useAuthStore()
// Create an authenticated fetch wrapper for the engine
function authFetch<T = any>(url: string, opts: any = {}): Promise<T> {
const headers = { ...opts.headers } as Record<string, string>
if (auth.accessToken) {
headers.Authorization = `Bearer ${auth.accessToken}`
}
return $fetch(url, { ...opts, headers }) as Promise<T>
}
const engine = useAtcEngine({ apiFetch: authFetch as any })
const screen = ref<'setup' | 'session'>('setup')
const processing = ref(false)
const isSpeaking = ref(false)
@@ -316,9 +332,8 @@ async function handleRecordingComplete(payload: { audio: string; format: string
processing.value = true
try {
// 1. STT via ptt endpoint
const sttRes = await $fetch<{ success: boolean; transcription: string }>('/api/atc/ptt', {
method: 'POST',
body: { audio: payload.audio, format: payload.format },
const sttRes = await api.post<{ success: boolean; transcription: string }>('/api/atc/ptt', {
audio: payload.audio, format: payload.format,
})
if (!sttRes.success || !sttRes.transcription) return
@@ -369,17 +384,14 @@ async function speakAtc(text: string) {
async function speakSingle(text: string) {
try {
const res = await $fetch<{
const res = await api.post<{
success: boolean
audio: { base64: string; mime: string }
normalized: string
}>('/api/atc/say', {
method: 'POST',
body: {
text,
level: settings.value.signalLevel,
speed: settings.value.speechSpeed,
},
text,
level: settings.value.signalLevel,
speed: settings.value.speechSpeed,
})
if (!res.success || !res.audio?.base64) return
@@ -431,10 +443,7 @@ function startTelemetryPoll() {
stopTelemetryPoll()
telemetryInterval = setInterval(async () => {
try {
const data = await $fetch<any>('/api/bridge/data', {
method: 'POST',
body: { status: 'poll' },
}).catch(() => null)
const data = await api.post<any>('/api/bridge/data', { status: 'poll' }).catch(() => null)
if (data && typeof data === 'object') {
telemetryConnected.value = true
engine.updateTelemetry({

View File

@@ -6,7 +6,7 @@ export default defineEventHandler(async (event) => {
if (!url.pathname.startsWith('/api/')) {
return
}
if (url.pathname.startsWith('/api/atc/')) {
if (url.pathname.startsWith('/api/atc/say')) {
return
}
if (url.pathname.startsWith('/api/service/')) {
@@ -15,7 +15,6 @@ export default defineEventHandler(async (event) => {
if (url.pathname.startsWith('/api/bridge/')) {
return
}
// /api/decision-flows/ was removed in Live ATC v2
if (event.node.req.method === 'OPTIONS') {
return
}

View File

@@ -42,7 +42,13 @@ function makeDefaultState(): EngineState {
}
}
export function useAtcEngine() {
export interface AtcEngineOptions {
/** Custom fetch function (e.g. with auth headers). Defaults to $fetch. */
apiFetch?: typeof $fetch
}
export function useAtcEngine(options: AtcEngineOptions = {}) {
const fetchFn = options.apiFetch ?? $fetch
const state = reactive<EngineState>(makeDefaultState())
const currentPhase = computed<Phase | undefined>(() => getPhase(state.currentPhase))
@@ -178,7 +184,7 @@ export function useAtcEngine() {
recentTransmissions: recentTx,
}
const res = await $fetch<RouteResponse>('/api/atc/route', {
const res = await fetchFn<RouteResponse>('/api/atc/route', {
method: 'POST',
body: req,
})
@@ -201,7 +207,7 @@ export function useAtcEngine() {
const destName = interaction.id === 'request_taxi' ? state.vars.runway : state.vars.arrival_stand
const airport = interaction.id === 'request_taxi' ? state.vars.dep : state.vars.dest
if (originName && destName && airport) {
const taxiRes = await $fetch<any>('/api/service/tools/taxiroute', {
const taxiRes = await fetchFn<any>('/api/service/tools/taxiroute', {
params: { airport, origin_name: originName, dest_name: destName },
})
if (taxiRes?.names_collapsed?.length) {