Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a98b5534b7 | |||
| 147d7f0d2c |
@@ -103,3 +103,8 @@
|
||||
* api streaming issues ([0621968](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/0621968c3ceddebe01e9c363bda345b5dcccfbbf))
|
||||
* api url ([2229cfd](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/2229cfd00a7d16daa6a9544c8940e792c4362dfb))
|
||||
* streaming issues ([bd6d023](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/bd6d02351336ed6adf66244979c6d959f47e318b))
|
||||
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.6.1...0.0.0) (2026-06-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* jwt token issue ([147d7f0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/147d7f0d2ca7a77bb80eb4b73b8d60b00ad2f708))
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
interface RegisterResponse {
|
||||
id: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TournamentAuthService {
|
||||
private readonly inflight = new Map<string, Promise<string>>();
|
||||
|
||||
async getToken(serverUrl: string): Promise<string> {
|
||||
const key = this.cacheKey(serverUrl);
|
||||
const cached = localStorage.getItem(key);
|
||||
if (cached) return cached;
|
||||
|
||||
const existing = this.inflight.get(key);
|
||||
if (existing) return existing;
|
||||
|
||||
const promise = this.register(serverUrl)
|
||||
.then(token => {
|
||||
localStorage.setItem(key, token);
|
||||
this.inflight.delete(key);
|
||||
return token;
|
||||
})
|
||||
.catch(err => {
|
||||
this.inflight.delete(key);
|
||||
throw err;
|
||||
});
|
||||
|
||||
this.inflight.set(key, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
clearToken(serverUrl: string): void {
|
||||
localStorage.removeItem(this.cacheKey(serverUrl));
|
||||
}
|
||||
|
||||
private async register(serverUrl: string): Promise<string> {
|
||||
const base = serverUrl.replace(/\/+$/, '');
|
||||
const localName = localStorage.getItem('username') ?? 'viewer';
|
||||
const res = await fetch(`${base}/api/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: `${localName}-watch`, isBot: false })
|
||||
});
|
||||
if (!res.ok) throw new Error(`tournament-server register failed: ${res.status}`);
|
||||
const body = (await res.json()) as RegisterResponse;
|
||||
return body.token;
|
||||
}
|
||||
|
||||
private cacheKey(serverUrl: string): string {
|
||||
return `tournament-token:${serverUrl.replace(/\/+$/, '')}`;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,38 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { GameStreamEvent, TournamentStreamEvent } from '../models/tournament.models';
|
||||
import { TournamentAuthService } from './tournament-auth.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TournamentStreamService {
|
||||
private readonly auth = inject(TournamentAuthService);
|
||||
|
||||
streamTournament(serverUrl: string, tournamentId: string): Observable<TournamentStreamEvent> {
|
||||
return this.ndjson<TournamentStreamEvent>(
|
||||
this.url(serverUrl, `/api/tournament/${tournamentId}/stream`)
|
||||
serverUrl,
|
||||
`/api/tournament/${tournamentId}/stream`
|
||||
);
|
||||
}
|
||||
|
||||
streamGame(serverUrl: string, tournamentId: string, gameId: string): Observable<GameStreamEvent> {
|
||||
return this.ndjson<GameStreamEvent>(
|
||||
this.url(serverUrl, `/api/tournament/${tournamentId}/game/${gameId}/stream`)
|
||||
serverUrl,
|
||||
`/api/tournament/${tournamentId}/game/${gameId}/stream`
|
||||
);
|
||||
}
|
||||
|
||||
private url(base: string, path: string): string {
|
||||
private fullUrl(base: string, path: string): string {
|
||||
if (!base) return path;
|
||||
return `${base.replace(/\/+$/, '')}${path}`;
|
||||
}
|
||||
|
||||
private ndjson<T>(url: string): Observable<T> {
|
||||
private ndjson<T>(serverUrl: string, path: string): Observable<T> {
|
||||
return new Observable<T>(subscriber => {
|
||||
const controller = new AbortController();
|
||||
const token = localStorage.getItem('token');
|
||||
const headers: Record<string, string> = { Accept: 'application/x-ndjson' };
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(url, { headers, signal: controller.signal });
|
||||
const res = await this.openWithRetry(serverUrl, path, controller.signal);
|
||||
if (!res.ok || !res.body) {
|
||||
subscriber.error(new Error(`Stream failed: ${res.status} ${res.statusText}`));
|
||||
return;
|
||||
@@ -63,4 +65,20 @@ export class TournamentStreamService {
|
||||
return () => controller.abort();
|
||||
});
|
||||
}
|
||||
|
||||
private async openWithRetry(serverUrl: string, path: string, signal: AbortSignal): Promise<Response> {
|
||||
const url = this.fullUrl(serverUrl, path);
|
||||
const token = await this.auth.getToken(serverUrl);
|
||||
const res = await fetch(url, {
|
||||
headers: { Accept: 'application/x-ndjson', Authorization: `Bearer ${token}` },
|
||||
signal,
|
||||
});
|
||||
if (res.status !== 401) return res;
|
||||
this.auth.clearToken(serverUrl);
|
||||
const fresh = await this.auth.getToken(serverUrl);
|
||||
return fetch(url, {
|
||||
headers: { Accept: 'application/x-ndjson', Authorization: `Bearer ${fresh}` },
|
||||
signal,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=6
|
||||
PATCH=1
|
||||
PATCH=2
|
||||
|
||||
Reference in New Issue
Block a user