frontend: attach auth credentials to all API requests

Add Authorization Bearer header to all fetch calls (apps, stream
start). Handle 401 responses by clearing token and redirecting to
login. Pass stream_token from the stream start response through to
the WebTransport URL as a query parameter for proxy authentication.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 02:38:18 +00:00
parent af9359bbdf
commit 786579a7d8
6 changed files with 36 additions and 11 deletions
+1
View File
@@ -25,6 +25,7 @@
streamStore.CertHash = streamData.CertHash;
streamStore.Width = streamData.Width;
streamStore.Height = streamData.Height;
streamStore.StreamToken = streamData.StreamToken;
console.log(`Stream data retrieved. Navigating to /stream.`);
await goto('/stream');
+11 -1
View File
@@ -1,3 +1,5 @@
import { getToken, handleUnauthorized } from './stores/authStore.svelte';
export interface App {
title: string;
id: number;
@@ -12,7 +14,15 @@ export interface AppsResponse {
export async function fetchApps() {
console.log('Getting apps');
const response = await fetch('/api/apps');
const response = await fetch('/api/apps', {
headers: { 'Authorization': `Bearer ${getToken()}` }
});
if (response.status === 401) {
handleUnauthorized();
throw new Error('Unauthorized');
}
console.log(response);
const data = (await response.json()) as AppsResponse;
console.log(data);
+16 -5
View File
@@ -1,8 +1,11 @@
import { getToken, handleUnauthorized } from './stores/authStore.svelte';
type StreamData = {
Url: string,
CertHash: Array<number>,
Width: number,
Height: number,
StreamToken: string,
}
export async function getStreamData(appId: number, server_name: string): Promise<StreamData> {
@@ -34,10 +37,16 @@ export async function getStreamData(appId: number, server_name: string): Promise
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`,
},
body: JSON.stringify(payload)
});
if (response.status === 401) {
handleUnauthorized();
throw new Error('Unauthorized');
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}: ${await response.text()}`);
}
@@ -45,15 +54,17 @@ export async function getStreamData(appId: number, server_name: string): Promise
const streamDataResp = await response.json();
console.log('Stream started:', streamDataResp);
let streamData: StreamData = { Url: streamDataResp.url, CertHash: streamDataResp.cert_hash, Width: width, Height: height };
let streamData: StreamData = {
Url: streamDataResp.url,
CertHash: streamDataResp.cert_hash,
Width: width,
Height: height,
StreamToken: streamDataResp.stream_token,
};
return streamData;
} catch (error) {
console.error('Error getting stream data: ', error);
throw new Error('Failed to start stream: ' + error);
}
}
@@ -3,4 +3,5 @@ export const streamStore = $state({
CertHash: [0],
Width: 0,
Height: 0,
StreamToken: '',
});
+2 -3
View File
@@ -6,6 +6,7 @@
$: certHash = streamStore.CertHash;
$: width = streamStore.Width;
$: height = streamStore.Height;
$: streamToken = streamStore.StreamToken;
</script>
<svelte:head>
@@ -13,9 +14,7 @@
<meta name="description" content="Streaming game" />
</svelte:head>
<!--<section>
</section>-->
<Stream {url} {certHash} {width} {height} />
<Stream {url} {certHash} {width} {height} {streamToken} />
<style>
section {
+5 -2
View File
@@ -8,17 +8,20 @@
certHash: Array<number>;
width: number;
height: number;
streamToken: string;
}
let { url, certHash, width, height }: Props = $props();
let { url, certHash, width, height, streamToken }: Props = $props();
let loading = $state(true);
let fullscreen = $state(false);
let gameplayView: HTMLDivElement;
let gameplayCanvas: HTMLCanvasElement;
async function startStream() {
// Append stream token to URL for proxy authentication
const authenticatedUrl = url + (url.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(streamToken);
await startWebtransportStream(
url,
authenticatedUrl,
certHash,
width,
height,