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 | 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 56x 56x 56x 56x 56x 57x 55x 57x 14x 3x 11x 2x 9x 48x 1x 47x 57x 11x 9x 2x 9x 9x 2x 2x 2x 57x 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;
}
}
|