mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-06 17:53:19 +08:00
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:
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user