Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | 24x 24x 24x 24x 24x 24x 24x 24x 24x 24x 59x 59x 59x 59x 59x 60x 58x 60x 14x 3x 11x 2x 9x 51x 1x 50x 60x 11x 9x 2x 9x 9x 2x 2x 2x 60x 1x 1x 2x 1x 1x | import { environment } from '../../../environments/environment';
import { isPlatformBrowser } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Injectable, Injector, PLATFORM_ID, inject } from '@angular/core';
import { LoggerService } from '@drevo-web/core';
import { CsrfResponse } from '@drevo-web/shared';
import { Observable, of, throwError } from 'rxjs';
import { map, catchError, tap, timeout, retry, shareReplay } from 'rxjs/operators';
const CSRF_TIMEOUT_MS = 10000;
const CSRF_RETRY_COUNT = 3;
/**
* Separate service for CSRF token management to avoid circular dependency
* between AuthInterceptor and AuthService.
*
* Uses Injector to lazily retrieve HttpClient, breaking the circular dependency:
* HttpClient → AuthInterceptor → CsrfService → HttpClient
*/
@Injectable({
providedIn: 'root',
})
export class CsrfService {
private readonly apiUrl = environment.apiUrl;
private readonly platformId = inject(PLATFORM_ID);
private readonly isBrowser = isPlatformBrowser(this.platformId);
private readonly logger = inject(LoggerService).withContext('CsrfService');
private readonly injector = inject(Injector);
private csrfToken: string | undefined;
private fetchInProgress$: Observable<string> | undefined;
private _httpClient: HttpClient | undefined;
/**
* Lazily get HttpClient to avoid circular dependency
*/
private get httpClient(): HttpClient {
if (!this._httpClient) {
this._httpClient = this.injector.get(HttpClient);
}
return this._httpClient;
}
getCsrfToken(): Observable<string> {
if (this.csrfToken) {
return of(this.csrfToken);
}
if (this.fetchInProgress$) {
return this.fetchInProgress$;
}
return this.fetchCsrfToken();
}
initCsrfToken(): void {
if (!this.isBrowser || this.csrfToken || this.fetchInProgress$) {
return;
}
this.fetchCsrfToken().subscribe();
}
private fetchCsrfToken(): Observable<string> {
this.fetchInProgress$ = this.httpClient
.get<CsrfResponse>(`${this.apiUrl}/api/auth/csrf`, {
withCredentials: true,
})
.pipe(
timeout(CSRF_TIMEOUT_MS),
retry(CSRF_RETRY_COUNT),
map(response => {
if (response.success && response.data?.csrfToken) {
return response.data.csrfToken;
}
throw new Error('Invalid CSRF response');
}),
tap(token => {
this.csrfToken = token;
this.fetchInProgress$ = undefined;
}),
catchError(error => {
this.logger.error('Failed to fetch CSRF token', error);
this.fetchInProgress$ = undefined;
return throwError(() => error);
}),
shareReplay(1)
);
return this.fetchInProgress$;
}
refreshCsrfToken(): Observable<string> {
this.clearToken();
return this.fetchCsrfToken();
}
updateCsrfToken(token: string): void {
this.csrfToken = token;
}
private clearToken(): void {
this.csrfToken = undefined;
this.fetchInProgress$ = undefined;
}
}
|