@nauth-toolkit/client-angular 0.1.53 → 0.1.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,124 @@
1
+ import { Injectable, Inject } from '@angular/core';
2
+ import {
3
+ HttpInterceptor,
4
+ HttpRequest,
5
+ HttpHandler,
6
+ HttpEvent,
7
+ HttpClient,
8
+ HttpErrorResponse,
9
+ } from '@angular/common/http';
10
+ import { Router } from '@angular/router';
11
+ import { Observable, catchError, switchMap, throwError, filter, take, BehaviorSubject, from } from 'rxjs';
12
+ import { NAUTH_CLIENT_CONFIG } from './tokens';
13
+ import { AuthService } from './auth.service';
14
+ import { NAuthClientConfig } from '@nauth-toolkit/client';
15
+
16
+ /**
17
+ * Refresh state management.
18
+ */
19
+ let isRefreshing = false;
20
+ const refreshTokenSubject = new BehaviorSubject<string | null>(null);
21
+ const retriedRequests = new WeakSet<HttpRequest<unknown>>();
22
+
23
+ /**
24
+ * Get CSRF token from cookie.
25
+ */
26
+ function getCsrfToken(cookieName: string): string | null {
27
+ if (typeof document === 'undefined') return null;
28
+ const match = document.cookie.match(new RegExp(`(^| )${cookieName}=([^;]+)`));
29
+ return match ? decodeURIComponent(match[2]) : null;
30
+ }
31
+
32
+ /**
33
+ * Class-based HTTP interceptor for NgModule apps (Angular < 17).
34
+ *
35
+ * For standalone components (Angular 17+), use the functional `authInterceptor` instead.
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * // app.module.ts
40
+ * import { HTTP_INTERCEPTORS } from '@angular/common/http';
41
+ * import { AuthInterceptor } from '@nauth-toolkit/client-angular';
42
+ *
43
+ * @NgModule({
44
+ * providers: [
45
+ * { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
46
+ * ]
47
+ * })
48
+ * ```
49
+ */
50
+ @Injectable()
51
+ export class AuthInterceptor implements HttpInterceptor {
52
+ constructor(
53
+ @Inject(NAUTH_CLIENT_CONFIG) private readonly config: NAuthClientConfig,
54
+ private readonly http: HttpClient,
55
+ private readonly authService: AuthService,
56
+ private readonly router: Router,
57
+ ) {}
58
+
59
+ intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
60
+ const tokenDelivery = this.config.tokenDelivery;
61
+ const baseUrl = this.config.baseUrl;
62
+
63
+ // ============================================================================
64
+ // COOKIES MODE: withCredentials + CSRF token
65
+ // ============================================================================
66
+ if (tokenDelivery === 'cookies') {
67
+ let clonedReq = req.clone({ withCredentials: true });
68
+
69
+ // Add CSRF token header if it's a mutating request
70
+ if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
71
+ const csrfToken = getCsrfToken(this.config.csrf?.cookieName || 'XSRF-TOKEN');
72
+ if (csrfToken) {
73
+ clonedReq = clonedReq.clone({
74
+ setHeaders: { [this.config.csrf?.headerName || 'X-XSRF-TOKEN']: csrfToken },
75
+ });
76
+ }
77
+ }
78
+
79
+ return next.handle(clonedReq).pipe(
80
+ catchError((error: HttpErrorResponse) => {
81
+ if (error.status === 401 && !retriedRequests.has(req)) {
82
+ retriedRequests.add(req);
83
+
84
+ if (!isRefreshing) {
85
+ isRefreshing = true;
86
+ refreshTokenSubject.next(null);
87
+
88
+ return from(
89
+ this.http
90
+ .post<{ accessToken?: string }>(`${baseUrl}/refresh`, {}, { withCredentials: true })
91
+ .toPromise(),
92
+ ).pipe(
93
+ switchMap(() => {
94
+ isRefreshing = false;
95
+ refreshTokenSubject.next('refreshed');
96
+ return next.handle(clonedReq);
97
+ }),
98
+ catchError((refreshError) => {
99
+ isRefreshing = false;
100
+ this.authService.logout();
101
+ this.router.navigate([this.config.redirects?.sessionExpired || '/login']);
102
+ return throwError(() => refreshError);
103
+ }),
104
+ );
105
+ } else {
106
+ return refreshTokenSubject.pipe(
107
+ filter((token) => token !== null),
108
+ take(1),
109
+ switchMap(() => next.handle(clonedReq)),
110
+ );
111
+ }
112
+ }
113
+
114
+ return throwError(() => error);
115
+ }),
116
+ );
117
+ }
118
+
119
+ // ============================================================================
120
+ // JSON MODE: Delegate to SDK for token handling
121
+ // ============================================================================
122
+ return next.handle(req);
123
+ }
124
+ }