|
1 | | -/* eslint-disable @typescript-eslint/no-explicit-any */ |
2 | 1 | import { |
3 | 2 | HttpErrorResponse, |
4 | 3 | HttpEvent, |
5 | 4 | HttpHandler, |
6 | 5 | HttpInterceptor, |
7 | 6 | HttpParameterCodec, |
| 7 | + HttpParams, |
8 | 8 | HttpRequest, |
9 | 9 | } from '@angular/common/http'; |
10 | 10 | import { Injectable } from '@angular/core'; |
11 | | -import { BehaviorSubject, Observable, from, of, throwError } from 'rxjs'; |
12 | | -import { catchError, filter, switchMap, take } from 'rxjs/operators'; |
13 | | -import * as dayjs from 'dayjs'; |
| 11 | + |
| 12 | +import { BehaviorSubject, Observable, forkJoin, from, iif, of, throwError } from 'rxjs'; |
| 13 | +import { catchError, concatMap, filter, mergeMap, take } from 'rxjs/operators'; |
| 14 | + |
14 | 15 | import { JwtHelperService } from '../services/jwt-helper.service'; |
| 16 | + |
| 17 | +import * as dayjs from 'dayjs'; |
15 | 18 | import { globalCacheBusterNotifier } from 'ts-cacheable'; |
16 | 19 | import { DeviceService } from '../services/device.service'; |
17 | 20 | import { RouterAuthService } from '../services/router-auth.service'; |
18 | 21 | import { SecureStorageService } from '../services/secure-storage.service'; |
19 | 22 | import { StorageService } from '../services/storage.service'; |
20 | 23 | import { TokenService } from '../services/token.service'; |
21 | 24 | import { UserEventService } from '../services/user-event.service'; |
| 25 | + |
22 | 26 | @Injectable() |
23 | 27 | export class HttpConfigInterceptor implements HttpInterceptor { |
24 | 28 | public accessTokenCallInProgress = false; |
25 | 29 |
|
26 | | - public accessTokenSubject = new BehaviorSubject<string | null>(null); |
| 30 | + public accessTokenSubject = new BehaviorSubject<string>(null); |
27 | 31 |
|
28 | 32 | constructor( |
29 | 33 | private jwtHelperService: JwtHelperService, |
@@ -51,115 +55,130 @@ export class HttpConfigInterceptor implements HttpInterceptor { |
51 | 55 | expiringSoon(accessToken: string): boolean { |
52 | 56 | try { |
53 | 57 | const expiryDate = dayjs(this.jwtHelperService.getExpirationDate(accessToken)); |
54 | | - const now = dayjs(); |
55 | | - const differenceSeconds = expiryDate.diff(now, 'seconds'); |
| 58 | + const now = dayjs(new Date()); |
| 59 | + const differenceSeconds = expiryDate.diff(now, 'second'); |
56 | 60 | const maxRefreshDifferenceSeconds = 2 * 60; |
57 | 61 | return differenceSeconds < maxRefreshDifferenceSeconds; |
58 | 62 | } catch (err) { |
59 | 63 | return true; |
60 | 64 | } |
61 | 65 | } |
62 | 66 |
|
63 | | - refreshAccessToken(): Observable<string | null> { |
| 67 | + refreshAccessToken(): Observable<string> { |
64 | 68 | return from(this.tokenService.getRefreshToken()).pipe( |
65 | | - switchMap((refreshToken) => { |
66 | | - if (refreshToken) { |
67 | | - return from(this.routerAuthService.fetchAccessToken(refreshToken)).pipe( |
68 | | - switchMap((authResponse) => from(this.routerAuthService.newAccessToken(authResponse.access_token))), |
69 | | - switchMap(() => from(this.tokenService.getAccessToken())), |
70 | | - catchError((error: HttpErrorResponse) => this.handleError(error)) // Handle refresh errors |
71 | | - ); |
72 | | - } else { |
73 | | - return of(null); |
74 | | - } |
75 | | - }) |
| 69 | + concatMap((refreshToken) => this.routerAuthService.fetchAccessToken(refreshToken)), |
| 70 | + catchError((error) => { |
| 71 | + this.userEventService.logout(); |
| 72 | + this.secureStorageService.clearAll(); |
| 73 | + this.storageService.clearAll(); |
| 74 | + globalCacheBusterNotifier.next(); |
| 75 | + return throwError(error); |
| 76 | + }), |
| 77 | + concatMap((authResponse) => this.routerAuthService.newAccessToken(authResponse.access_token)), |
| 78 | + concatMap(() => from(this.tokenService.getAccessToken())) |
76 | 79 | ); |
77 | 80 | } |
78 | 81 |
|
79 | | - handleError(error: HttpErrorResponse): Observable<never> { |
80 | | - if (error.status === 401) { |
81 | | - this.userEventService.logout(); |
82 | | - this.secureStorageService.clearAll(); |
83 | | - this.storageService.clearAll(); |
84 | | - globalCacheBusterNotifier.next(); |
85 | | - } |
86 | | - return throwError(error); // Rethrow the error to the caller |
87 | | - } |
88 | | - |
89 | 82 | /** |
90 | 83 | * This method get current accessToken from Storage, check if this token is expiring or not. |
91 | 84 | * If the token is expiring it will get another accessToken from API and return the new accessToken |
92 | 85 | * If multiple API call initiated then `this.accessTokenCallInProgress` will block multiple access_token call |
93 | 86 | * Reference: https://stackoverflow.com/a/57638101 |
94 | 87 | */ |
95 | | - getAccessToken(): Observable<string | null> { |
| 88 | + getAccessToken(): Observable<string> { |
96 | 89 | return from(this.tokenService.getAccessToken()).pipe( |
97 | | - switchMap((accessToken) => { |
98 | | - if (accessToken && !this.expiringSoon(accessToken)) { |
99 | | - return of(accessToken); |
100 | | - } |
101 | | - if (!this.accessTokenCallInProgress) { |
102 | | - this.accessTokenCallInProgress = true; |
103 | | - this.accessTokenSubject.next(null); |
104 | | - return this.refreshAccessToken().pipe( |
105 | | - switchMap((newAccessToken) => { |
106 | | - this.accessTokenCallInProgress = false; |
107 | | - this.accessTokenSubject.next(newAccessToken); |
108 | | - return of(newAccessToken); |
109 | | - }) |
110 | | - ); |
| 90 | + concatMap((accessToken) => { |
| 91 | + if (this.expiringSoon(accessToken)) { |
| 92 | + if (!this.accessTokenCallInProgress) { |
| 93 | + this.accessTokenCallInProgress = true; |
| 94 | + this.accessTokenSubject.next(null); |
| 95 | + return this.refreshAccessToken().pipe( |
| 96 | + concatMap((newAccessToken) => { |
| 97 | + this.accessTokenCallInProgress = false; |
| 98 | + this.accessTokenSubject.next(newAccessToken); |
| 99 | + return of(newAccessToken); |
| 100 | + }) |
| 101 | + ); |
| 102 | + } else { |
| 103 | + return this.accessTokenSubject.pipe( |
| 104 | + filter((result) => result !== null), |
| 105 | + take(1), |
| 106 | + concatMap(() => from(this.tokenService.getAccessToken())) |
| 107 | + ); |
| 108 | + } |
111 | 109 | } else { |
112 | | - // If a refresh is already in progress, wait for it to complete |
113 | | - return this.accessTokenSubject.pipe( |
114 | | - filter((result) => result !== null), |
115 | | - take(1), |
116 | | - switchMap(() => from(this.tokenService.getAccessToken())) |
117 | | - ); |
| 110 | + return of(accessToken); |
118 | 111 | } |
119 | 112 | }) |
120 | 113 | ); |
121 | 114 | } |
122 | 115 |
|
123 | 116 | getUrlWithoutQueryParam(url: string): string { |
124 | | - // Remove query parameters from the URL |
125 | | - return url.split('?')[0].split(';')[0].substring(0, 200); |
126 | | - } |
127 | | - |
128 | | - intercept(request: HttpRequest<string>, next: HttpHandler): Observable<HttpEvent<string>> { |
129 | | - if (this.secureUrl(request.url)) { |
130 | | - return this.getAccessToken().pipe( |
131 | | - switchMap((accessToken) => { |
132 | | - if (!accessToken) { |
133 | | - return this.handleError({ status: 401, error: 'Unauthorized' } as HttpErrorResponse); |
134 | | - } |
135 | | - return this.executeHttpRequest(request, next, accessToken); |
136 | | - }) |
137 | | - ); |
138 | | - } else { |
139 | | - return next.handle(request); |
| 117 | + const queryIndex = Math.min( |
| 118 | + url.indexOf('?') !== -1 ? url.indexOf('?') : url.length, |
| 119 | + url.indexOf(';') !== -1 ? url.indexOf(';') : url.length |
| 120 | + ); |
| 121 | + if (queryIndex !== url.length) { |
| 122 | + url = url.substring(0, queryIndex); |
| 123 | + } |
| 124 | + if (url.length > 200) { |
| 125 | + url = url.substring(0, 200); |
140 | 126 | } |
| 127 | + return url; |
141 | 128 | } |
142 | 129 |
|
143 | | - executeHttpRequest(request: HttpRequest<any>, next: HttpHandler, accessToken: string): Observable<HttpEvent<any>> { |
144 | | - return from(this.deviceService.getDeviceInfo()).pipe( |
145 | | - switchMap((deviceInfo) => { |
| 130 | + intercept(request: HttpRequest<string>, next: HttpHandler): Observable<HttpEvent<string>> { |
| 131 | + return forkJoin({ |
| 132 | + token: iif(() => this.secureUrl(request.url), this.getAccessToken(), of(null)), |
| 133 | + deviceInfo: from(this.deviceService.getDeviceInfo()), |
| 134 | + }).pipe( |
| 135 | + concatMap(({ token, deviceInfo }) => { |
| 136 | + if (token && this.secureUrl(request.url)) { |
| 137 | + request = request.clone({ headers: request.headers.set('Authorization', 'Bearer ' + token) }); |
| 138 | + const params = new HttpParams({ encoder: new CustomEncoder(), fromString: request.params.toString() }); |
| 139 | + request = request.clone({ params }); |
| 140 | + } |
146 | 141 | const appVersion = deviceInfo.appVersion || '0.0.0'; |
147 | 142 | const osVersion = deviceInfo.osVersion; |
148 | 143 | const operatingSystem = deviceInfo.operatingSystem; |
149 | | - const mobileModifiedAppVersion = `fyle-mobile::${appVersion}::${operatingSystem}::${osVersion}`; |
| 144 | + const mobileModifiedappVersion = `fyle-mobile::${appVersion}::${operatingSystem}::${osVersion}`; |
150 | 145 | request = request.clone({ |
151 | | - headers: request.headers.set('Authorization', `Bearer ${accessToken}`), |
152 | 146 | setHeaders: { |
153 | | - 'X-App-Version': mobileModifiedAppVersion, |
| 147 | + 'X-App-Version': mobileModifiedappVersion, |
154 | 148 | 'X-Page-Url': this.getUrlWithoutQueryParam(window.location.href), |
155 | 149 | 'X-Source-Identifier': 'mobile_app', |
156 | 150 | }, |
157 | 151 | }); |
158 | | - return next.handle(request).pipe(catchError((error: HttpErrorResponse) => this.handleError(error))); |
| 152 | + |
| 153 | + return next.handle(request).pipe( |
| 154 | + catchError((error) => { |
| 155 | + if (error instanceof HttpErrorResponse) { |
| 156 | + if (this.expiringSoon(token)) { |
| 157 | + return from(this.refreshAccessToken()).pipe( |
| 158 | + mergeMap((newToken) => { |
| 159 | + request = request.clone({ headers: request.headers.set('Authorization', 'Bearer ' + newToken) }); |
| 160 | + return next.handle(request); |
| 161 | + }) |
| 162 | + ); |
| 163 | + } else if ( |
| 164 | + (error.status === 404 && error.headers.get('X-Mobile-App-Blocked') === 'true') || |
| 165 | + error.status === 401 |
| 166 | + ) { |
| 167 | + this.userEventService.logout(); |
| 168 | + this.secureStorageService.clearAll(); |
| 169 | + this.storageService.clearAll(); |
| 170 | + globalCacheBusterNotifier.next(); |
| 171 | + return throwError(error); |
| 172 | + } |
| 173 | + } |
| 174 | + return throwError(error); |
| 175 | + }) |
| 176 | + ); |
159 | 177 | }) |
160 | 178 | ); |
161 | 179 | } |
162 | 180 | } |
| 181 | + |
163 | 182 | export class CustomEncoder implements HttpParameterCodec { |
164 | 183 | encodeKey(key: string): string { |
165 | 184 | return encodeURIComponent(key); |
|
0 commit comments