Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74f82bc0ba | |||
| 1d2c217da8 |
@@ -71,3 +71,8 @@
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** attach Bearer token to /api/bots requests ([#12](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/12)) ([a54957a](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/a54957aa74ef15bf2dd439d386e221ac134c5c5c))
|
||||
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.4.1...0.0.0) (2026-06-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* NCS-122 send WS token via first-message auth instead of query param ([#13](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/13)) ([1d2c217](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/1d2c217da8982d361e2eb7de26f6447171a1dd43))
|
||||
|
||||
@@ -6,110 +6,115 @@ import { ChallengeService } from './challenge.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ChallengeWebSocketService {
|
||||
private readonly challengeEventService = inject(ChallengeEventService);
|
||||
private readonly challengeService = inject(ChallengeService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly challengeEventService = inject(ChallengeEventService);
|
||||
private readonly challengeService = inject(ChallengeService);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
private ws: WebSocket | null = null;
|
||||
private reconnectAttempts = 0;
|
||||
private readonly maxReconnectAttempts = 5;
|
||||
private readonly reconnectDelay = 3000;
|
||||
private intentionalClose = false;
|
||||
private ws: WebSocket | null = null;
|
||||
private reconnectAttempts = 0;
|
||||
private readonly maxReconnectAttempts = 5;
|
||||
private readonly reconnectDelay = 3000;
|
||||
private intentionalClose = false;
|
||||
|
||||
connect(): void {
|
||||
if (this.ws) return;
|
||||
connect(): void {
|
||||
if (this.ws) return;
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
|
||||
const url = `${environment.userWsBaseUrl}/api/user/ws?token=${encodeURIComponent(token)}`;
|
||||
const url = `${environment.userWsBaseUrl}/api/user/ws`;
|
||||
|
||||
try {
|
||||
this.intentionalClose = false;
|
||||
this.ws = new WebSocket(url);
|
||||
try {
|
||||
this.intentionalClose = false;
|
||||
this.ws = new WebSocket(url);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.reconnectAttempts = 0;
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
this.handleMessage(event.data as string);
|
||||
};
|
||||
|
||||
this.ws.onerror = () => {
|
||||
// onclose fires right after, handles reconnect
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.ws = null;
|
||||
if (!this.intentionalClose) {
|
||||
this.attemptReconnect();
|
||||
}
|
||||
};
|
||||
} catch {
|
||||
this.attemptReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this.intentionalClose = true;
|
||||
this.ws.onopen = () => {
|
||||
this.reconnectAttempts = 0;
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
this.ws?.send(JSON.stringify({ type: 'auth', token }));
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
this.handleMessage(event.data as string);
|
||||
};
|
||||
|
||||
this.ws.onerror = () => {
|
||||
// onclose fires right after, handles reconnect
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.ws = null;
|
||||
if (!this.intentionalClose) {
|
||||
this.attemptReconnect();
|
||||
}
|
||||
};
|
||||
} catch {
|
||||
this.attemptReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this.intentionalClose = true;
|
||||
this.reconnectAttempts = 0;
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleMessage(data: string): void {
|
||||
let message: Record<string, unknown>;
|
||||
try {
|
||||
message = JSON.parse(data) as Record<string, unknown>;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
private handleMessage(data: string): void {
|
||||
let message: Record<string, unknown>;
|
||||
try {
|
||||
message = JSON.parse(data) as Record<string, unknown>;
|
||||
} catch {
|
||||
return;
|
||||
switch (message['type']) {
|
||||
case 'CONNECTED':
|
||||
break;
|
||||
|
||||
case 'challengeCreated': {
|
||||
const challengeId = message['challengeId'] as string | undefined;
|
||||
if (challengeId) {
|
||||
this.challengeService.getChallenge(challengeId).subscribe({
|
||||
next: (challenge) => this.challengeEventService.onChallengeReceived(challenge),
|
||||
error: () => {
|
||||
/* challenge may have already expired */
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
switch (message['type']) {
|
||||
case 'CONNECTED':
|
||||
break;
|
||||
|
||||
case 'challengeCreated': {
|
||||
const challengeId = message['challengeId'] as string | undefined;
|
||||
if (challengeId) {
|
||||
this.challengeService.getChallenge(challengeId).subscribe({
|
||||
next: challenge => this.challengeEventService.onChallengeReceived(challenge),
|
||||
error: () => { /* challenge may have already expired */ }
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'challengeAccepted': {
|
||||
const challengeId = message['challengeId'] as string | undefined;
|
||||
const gameId = message['gameId'] as string | undefined;
|
||||
if (challengeId) {
|
||||
this.challengeEventService.removeChallenge(challengeId);
|
||||
}
|
||||
if (gameId) {
|
||||
void this.router.navigate(['/game', gameId]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'challengeDeclined':
|
||||
case 'challengeExpired':
|
||||
case 'challengeCancelled': {
|
||||
const challengeId = message['challengeId'] as string | undefined;
|
||||
if (challengeId) {
|
||||
this.challengeEventService.removeChallenge(challengeId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'challengeAccepted': {
|
||||
const challengeId = message['challengeId'] as string | undefined;
|
||||
const gameId = message['gameId'] as string | undefined;
|
||||
if (challengeId) {
|
||||
this.challengeEventService.removeChallenge(challengeId);
|
||||
}
|
||||
}
|
||||
if (gameId) {
|
||||
void this.router.navigate(['/game', gameId]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
private attemptReconnect(): void {
|
||||
if (this.intentionalClose || this.reconnectAttempts >= this.maxReconnectAttempts) return;
|
||||
this.reconnectAttempts++;
|
||||
setTimeout(() => { this.connect(); }, this.reconnectDelay);
|
||||
case 'challengeDeclined':
|
||||
case 'challengeExpired':
|
||||
case 'challengeCancelled': {
|
||||
const challengeId = message['challengeId'] as string | undefined;
|
||||
if (challengeId) {
|
||||
this.challengeEventService.removeChallenge(challengeId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private attemptReconnect(): void {
|
||||
if (this.intentionalClose || this.reconnectAttempts >= this.maxReconnectAttempts) return;
|
||||
this.reconnectAttempts++;
|
||||
setTimeout(() => {
|
||||
this.connect();
|
||||
}, this.reconnectDelay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
GameState,
|
||||
GameStreamEvent,
|
||||
LegalMovesResponse,
|
||||
PlayerInfo
|
||||
PlayerInfo,
|
||||
} from '../models/game.models';
|
||||
import { AnalysisRequest, AnalysisResponse } from '../models/analysis.models';
|
||||
import { StreamHandlerService } from './stream-handler.service';
|
||||
@@ -29,11 +29,11 @@ export class GameApiService {
|
||||
const playerColor = Math.random() > 0.5 ? 'white' : 'black';
|
||||
const playerInfo: PlayerInfo = {
|
||||
id: `player-${Date.now()}`,
|
||||
displayName: 'You'
|
||||
displayName: 'You',
|
||||
};
|
||||
const botInfo: PlayerInfo = {
|
||||
id: `bot-${difficulty}`,
|
||||
displayName: `Bot (${difficulty})`
|
||||
displayName: `Bot (${difficulty})`,
|
||||
};
|
||||
|
||||
const payload =
|
||||
@@ -57,7 +57,9 @@ export class GameApiService {
|
||||
if (square) {
|
||||
params = params.set('square', square);
|
||||
}
|
||||
return this.http.get<LegalMovesResponse>(`${this.apiBase}${this.apiPath}/${gameId}/moves`, { params });
|
||||
return this.http.get<LegalMovesResponse>(`${this.apiBase}${this.apiPath}/${gameId}/moves`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
importFen(fen: string): Observable<GameFull> {
|
||||
@@ -90,11 +92,8 @@ export class GameApiService {
|
||||
}
|
||||
|
||||
streamGame(gameId: string): Observable<GameStreamEvent> {
|
||||
const token = localStorage.getItem('token');
|
||||
let wsUrl = `${this.resolveWsBase()}${this.apiPath}/${gameId}/ws`;
|
||||
if (token) {
|
||||
wsUrl += `?token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
return this.streamHandler.createGameStream(wsUrl, gameId);
|
||||
const wsUrl = `${this.resolveWsBase()}${this.apiPath}/${gameId}/ws`;
|
||||
const token = localStorage.getItem('token') ?? '';
|
||||
return this.streamHandler.createGameStream(wsUrl, gameId, token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const WS_CONNECT_TIMEOUT_MS = 3000;
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class StreamHandlerService {
|
||||
createGameStream(wsUrl: string, gameId: string): Observable<GameStreamEvent> {
|
||||
createGameStream(wsUrl: string, gameId: string, token: string): Observable<GameStreamEvent> {
|
||||
return new Observable<GameStreamEvent>((observer) => {
|
||||
const ws = new WebSocket(wsUrl);
|
||||
let connected = false;
|
||||
@@ -14,7 +14,7 @@ export class StreamHandlerService {
|
||||
const emitErrorEvent = (message: string): void => {
|
||||
const errorEvent: ErrorEvent = {
|
||||
type: 'error',
|
||||
error: { code: 'STREAM_ERROR', message }
|
||||
error: { code: 'STREAM_ERROR', message },
|
||||
};
|
||||
observer.next(errorEvent);
|
||||
};
|
||||
@@ -36,6 +36,7 @@ export class StreamHandlerService {
|
||||
connected = true;
|
||||
clearTimeout(connectionTimeoutId);
|
||||
console.log(`[StreamHandler] WebSocket connected for ${gameId}`);
|
||||
ws.send(JSON.stringify({ type: 'auth', token }));
|
||||
};
|
||||
|
||||
ws.onmessage = (message) => {
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=4
|
||||
PATCH=1
|
||||
PATCH=2
|
||||
|
||||
Reference in New Issue
Block a user