fix: NCWF-4 Token Issues (#8)

Co-authored-by: Lala, Shahd <Shahd.Lala@sybit.de>
Co-authored-by: shahdlala66 <shahd.lala66@gmail.com>
Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
2026-06-02 21:55:55 +02:00
parent 873bfe3bae
commit 95eff42dfe
37 changed files with 2522 additions and 573 deletions
+3 -2
View File
@@ -8,9 +8,10 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
req.url.includes('/api/account/bots') ||
req.url.includes('/api/account/official-bots') ||
req.url.includes('/api/board/game') ||
req.url.includes('/api/challenge');
req.url.includes('/api/challenge') ||
req.url.includes('/api/tournament');
if (token && isProtectedEndpoint) {
if (token && isProtectedEndpoint && !req.headers.has('Authorization')) {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
+3 -2
View File
@@ -26,9 +26,9 @@ export class AuthService {
})
.pipe(
tap((response) => {
localStorage.setItem('token', response.token);
localStorage.setItem('token', response.accessToken); //GRRRRRRRRRR
localStorage.setItem('refreshToken', response.refreshToken);
localStorage.setItem('username', username);
// After login, fetch current user info
this.getCurrentUser().subscribe();
})
);
@@ -60,6 +60,7 @@ export class AuthService {
logout(): void {
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
localStorage.removeItem('username');
localStorage.removeItem('userId');
this.currentUserSubject.next(null);
+27
View File
@@ -0,0 +1,27 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, map } from 'rxjs';
import { Bot, BotWithToken } from '../models/bot.models';
@Injectable({ providedIn: 'root' })
export class BotService {
private readonly http = inject(HttpClient);
private readonly base = '/api/account/bots';
list(): Observable<Bot[]> {
return this.http.get<Bot[]>(this.base);
}
create(name: string): Observable<BotWithToken> {
return this.http.post<BotWithToken>(this.base, { name });
}
rotateToken(botId: string): Observable<string> {
return this.http.post<{ token: string }>(`${this.base}/${botId}/rotate-token`, null)
.pipe(map(r => r.token));
}
delete(botId: string): Observable<void> {
return this.http.delete<void>(`${this.base}/${botId}`);
}
}
+13 -1
View File
@@ -67,6 +67,14 @@ export class GameApiService {
return this.http.post<GameFull>(`${this.apiBase}${this.apiPath}/import/pgn`, { pgn });
}
resignGame(gameId: string): Observable<void> {
return this.http.post<void>(`${this.apiBase}${this.apiPath}/${gameId}/resign`, {});
}
offerDraw(gameId: string): Observable<void> {
return this.http.post<void>(`${this.apiBase}${this.apiPath}/${gameId}/draw/offer`, {});
}
private resolveWsBase(): string {
if (this.wsBase) {
return this.wsBase;
@@ -77,7 +85,11 @@ export class GameApiService {
}
streamGame(gameId: string): Observable<GameStreamEvent> {
const wsUrl = `${this.resolveWsBase()}${this.apiPath}/${gameId}/ws`;
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);
}
}
+14 -9
View File
@@ -24,23 +24,28 @@ export class GameCompletionService {
return { isFinished: true, message };
}
isTimeOut(state: GameState | null): boolean {
if (!state?.clock) return false;
return state.clock.whiteRemainingMs <= 0 || state.clock.blackRemainingMs <= 0;
}
private buildCompletionMessage(status: GameStatus, state: GameState, game: GameFull): string {
const winner = state.winner === 'white' ? game.white.displayName : state.winner === 'black' ? game.black.displayName : null;
const loser = state.winner === 'white' ? game.black.displayName : state.winner === 'black' ? game.white.displayName : null;
switch (status) {
case 'checkmate':
const winner = state.winner === 'white' ? game.white.displayName : game.black.displayName;
return `Checkmate! ${winner} wins!`;
return winner ? `Checkmate — ${winner} wins!` : 'Checkmate!';
case 'stalemate':
return 'Stalemate! The game is a draw.';
return 'Stalemate draw!';
case 'resign':
const resignedPlayer = state.winner === 'white' ? game.black.displayName : game.white.displayName;
const resignedWinner = state.winner === 'white' ? game.white.displayName : game.black.displayName;
return `${resignedPlayer} resigned. ${resignedWinner} wins!`;
return loser && winner ? `${loser} resigned — ${winner} wins!` : 'Resigned.';
case 'draw':
return 'Draw! The game ended in a draw.';
return 'Draw by agreement.';
case 'insufficientMaterial':
return 'Insufficient material! The game is a draw.';
return 'Draw — insufficient material.';
default:
return 'Game ended!';
return 'Game over.';
}
}
}
+52
View File
@@ -0,0 +1,52 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Tournament, TournamentList, RoundPairings } from '../models/tournament.models';
export interface CreateTournamentForm {
name: string;
nbRounds: number;
clockLimitMinutes: number;
clockIncrement: number;
rated: boolean;
}
@Injectable({ providedIn: 'root' })
export class TournamentService {
private readonly http = inject(HttpClient);
private readonly base = '/api/tournament';
list(): Observable<TournamentList> {
return this.http.get<TournamentList>(this.base);
}
get(id: string): Observable<Tournament> {
return this.http.get<Tournament>(`${this.base}/${id}`);
}
create(form: CreateTournamentForm): Observable<Tournament> {
const body = new URLSearchParams();
body.set('name', form.name);
body.set('nbRounds', String(form.nbRounds));
body.set('clockLimit', String(form.clockLimitMinutes * 60));
body.set('clockIncrement', String(form.clockIncrement));
body.set('rated', String(form.rated));
return this.http.post<Tournament>(this.base, body.toString(), {
headers: new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' })
});
}
start(id: string): Observable<Tournament> {
return this.http.post<Tournament>(`${this.base}/${id}/start`, null);
}
joinWithBotToken(id: string, botToken: string): Observable<void> {
return this.http.post<void>(`${this.base}/${id}/join`, null, {
headers: new HttpHeaders({ Authorization: `Bearer ${botToken}` })
});
}
roundPairings(id: string, round: number): Observable<RoundPairings> {
return this.http.get<RoundPairings>(`${this.base}/${id}/round/${round}`);
}
}