@goat-bravos/shared-lib-client 1.0.3 → 1.0.5
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.
- package/dist/enums/error-code.enum.d.ts +2 -1
- package/dist/enums/error-code.enum.d.ts.map +1 -1
- package/dist/enums/error-code.enum.js +1 -0
- package/dist/interceptors/auth.interceptor.d.ts +45 -0
- package/dist/interceptors/auth.interceptor.d.ts.map +1 -1
- package/dist/interceptors/auth.interceptor.js +85 -10
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ export declare enum ErrorCode {
|
|
|
7
7
|
VALIDATION_ERROR = "validation.error",
|
|
8
8
|
CONFLICT = "conflict",
|
|
9
9
|
SERVICE_UNAVAILABLE = "service.unavailable",
|
|
10
|
-
REFRESH_TOKEN_INVALID = "auth.exception.refresh_token_invalid"
|
|
10
|
+
REFRESH_TOKEN_INVALID = "auth.exception.refresh_token_invalid",
|
|
11
|
+
ACCESS_TOKEN_EXPIRED = "auth.exception.access_token_expired"
|
|
11
12
|
}
|
|
12
13
|
//# sourceMappingURL=error-code.enum.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-code.enum.d.ts","sourceRoot":"","sources":["../../src/enums/error-code.enum.ts"],"names":[],"mappings":"AAAA,oBAAY,SAAS;IACnB,kBAAkB,uBAAuB;IACzC,YAAY,iBAAiB;IAC7B,SAAS,cAAc;IACvB,WAAW,gBAAgB;IAC3B,qBAAqB,0BAA0B;IAC/C,gBAAgB,qBAAqB;IACrC,QAAQ,aAAa;IACrB,mBAAmB,wBAAwB;IAC3C,qBAAqB,yCAAyC;
|
|
1
|
+
{"version":3,"file":"error-code.enum.d.ts","sourceRoot":"","sources":["../../src/enums/error-code.enum.ts"],"names":[],"mappings":"AAAA,oBAAY,SAAS;IACnB,kBAAkB,uBAAuB;IACzC,YAAY,iBAAiB;IAC7B,SAAS,cAAc;IACvB,WAAW,gBAAgB;IAC3B,qBAAqB,0BAA0B;IAC/C,gBAAgB,qBAAqB;IACrC,QAAQ,aAAa;IACrB,mBAAmB,wBAAwB;IAC3C,qBAAqB,yCAAyC;IAC9D,oBAAoB,wCAAwC;CAC7D"}
|
|
@@ -9,4 +9,5 @@ export var ErrorCode;
|
|
|
9
9
|
ErrorCode["CONFLICT"] = "conflict";
|
|
10
10
|
ErrorCode["SERVICE_UNAVAILABLE"] = "service.unavailable";
|
|
11
11
|
ErrorCode["REFRESH_TOKEN_INVALID"] = "auth.exception.refresh_token_invalid";
|
|
12
|
+
ErrorCode["ACCESS_TOKEN_EXPIRED"] = "auth.exception.access_token_expired";
|
|
12
13
|
})(ErrorCode || (ErrorCode = {}));
|
|
@@ -1,4 +1,43 @@
|
|
|
1
1
|
import { HttpInterceptorFn } from "@angular/common/http";
|
|
2
|
+
export declare const CANCEL_REFRESH_TOKEN = "CANCEL_REFRESH_TOKEN";
|
|
3
|
+
/**
|
|
4
|
+
* Cấu hình cho auth interceptor.
|
|
5
|
+
*/
|
|
6
|
+
export interface AuthInterceptorConfig {
|
|
7
|
+
/**
|
|
8
|
+
* Danh sách các path pattern sẽ KHÔNG gắn Bearer token.
|
|
9
|
+
* Mỗi phần tử là một chuỗi mà interceptor sẽ kiểm tra bằng `url.includes(pattern)`.
|
|
10
|
+
*
|
|
11
|
+
* Ví dụ: ['/login', '/password-reset', '/public/products']
|
|
12
|
+
*/
|
|
13
|
+
excludedPaths?: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Cấu hình auth interceptor từ bên ngoài (thường gọi ở Shell App).
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Trong app.config.ts của Shell App:
|
|
20
|
+
* import { configureAuthInterceptor } from 'shared-lib-client';
|
|
21
|
+
*
|
|
22
|
+
* configureAuthInterceptor({
|
|
23
|
+
* excludedPaths: [
|
|
24
|
+
* '/login',
|
|
25
|
+
* '/password-reset',
|
|
26
|
+
* '/refresh',
|
|
27
|
+
* '/public/products',
|
|
28
|
+
* '/public/categories',
|
|
29
|
+
* ],
|
|
30
|
+
* });
|
|
31
|
+
*/
|
|
32
|
+
export declare function configureAuthInterceptor(config: AuthInterceptorConfig): void;
|
|
33
|
+
/**
|
|
34
|
+
* Thêm các path vào danh sách loại trừ mà không ghi đè danh sách mặc định.
|
|
35
|
+
*/
|
|
36
|
+
export declare function addExcludedPaths(paths: string[]): void;
|
|
37
|
+
/**
|
|
38
|
+
* Lấy danh sách các path đang bị loại trừ (dùng cho debug/testing).
|
|
39
|
+
*/
|
|
40
|
+
export declare function getExcludedPaths(): string[];
|
|
2
41
|
/**
|
|
3
42
|
* Auth interceptor dùng chung cho các micro-frontend.
|
|
4
43
|
* - Tự gắn header Authorization nếu có access token
|
|
@@ -11,4 +50,10 @@ export declare const authInterceptor: HttpInterceptorFn;
|
|
|
11
50
|
* để đánh thức các request đang chờ.
|
|
12
51
|
*/
|
|
13
52
|
export declare function notifyTokenRefreshed(newToken: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Được Shell/Auth MFE gọi khi refresh token thất bại
|
|
55
|
+
* để giải phóng toàn bộ các request đang nằm chờ trong hàng đợi
|
|
56
|
+
* tránh rò rỉ bộ nhớ (memory leak).
|
|
57
|
+
*/
|
|
58
|
+
export declare function cancelTokenRefresh(): void;
|
|
14
59
|
//# sourceMappingURL=auth.interceptor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.interceptor.d.ts","sourceRoot":"","sources":["../../src/interceptors/auth.interceptor.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,iBAAiB,EAClB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.interceptor.d.ts","sourceRoot":"","sources":["../../src/interceptors/auth.interceptor.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,iBAAiB,EAClB,MAAM,sBAAsB,CAAC;AAa9B,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAM3D;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAWD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAI5E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAGtD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,EAAE,CAE3C;AAaD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,iBAuC7B,CAAC;AA8DF;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAG3D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC"}
|
|
@@ -2,9 +2,65 @@ import { throwError, BehaviorSubject } from "rxjs";
|
|
|
2
2
|
import { catchError, filter, take, switchMap, finalize } from "rxjs/operators";
|
|
3
3
|
import { StorageUtil } from "../utils/storage.util";
|
|
4
4
|
import { ErrorCode } from "../enums/error-code.enum";
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// CONFIGURATION & STATE
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Hằng số nội bộ dùng để nhận diện tín hiệu hủy làm mới token.
|
|
9
|
+
export const CANCEL_REFRESH_TOKEN = "CANCEL_REFRESH_TOKEN";
|
|
5
10
|
// Cờ và subject dùng để đồng bộ các request khi access token hết hạn.
|
|
6
11
|
let isRefreshing = false;
|
|
7
12
|
const refreshTokenSubject = new BehaviorSubject(null);
|
|
13
|
+
/**
|
|
14
|
+
* Danh sách path đang được sử dụng (có thể được cấu hình từ bên ngoài).
|
|
15
|
+
*/
|
|
16
|
+
let activeExcludedPaths = [];
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// PUBLIC API (CONFIGURATION & UTILS)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* Cấu hình auth interceptor từ bên ngoài (thường gọi ở Shell App).
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // Trong app.config.ts của Shell App:
|
|
25
|
+
* import { configureAuthInterceptor } from 'shared-lib-client';
|
|
26
|
+
*
|
|
27
|
+
* configureAuthInterceptor({
|
|
28
|
+
* excludedPaths: [
|
|
29
|
+
* '/login',
|
|
30
|
+
* '/password-reset',
|
|
31
|
+
* '/refresh',
|
|
32
|
+
* '/public/products',
|
|
33
|
+
* '/public/categories',
|
|
34
|
+
* ],
|
|
35
|
+
* });
|
|
36
|
+
*/
|
|
37
|
+
export function configureAuthInterceptor(config) {
|
|
38
|
+
if (config.excludedPaths && config.excludedPaths.length > 0) {
|
|
39
|
+
activeExcludedPaths = [...config.excludedPaths];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Thêm các path vào danh sách loại trừ mà không ghi đè danh sách mặc định.
|
|
44
|
+
*/
|
|
45
|
+
export function addExcludedPaths(paths) {
|
|
46
|
+
const newPaths = paths.filter((p) => !activeExcludedPaths.includes(p));
|
|
47
|
+
activeExcludedPaths = [...activeExcludedPaths, ...newPaths];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Lấy danh sách các path đang bị loại trừ (dùng cho debug/testing).
|
|
51
|
+
*/
|
|
52
|
+
export function getExcludedPaths() {
|
|
53
|
+
return [...activeExcludedPaths];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Kiểm tra một URL có thuộc danh sách loại trừ hay không.
|
|
57
|
+
*/
|
|
58
|
+
function isExcluded(url) {
|
|
59
|
+
return activeExcludedPaths.some((pattern) => url.includes(pattern));
|
|
60
|
+
}
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// INTERCEPTOR CORE
|
|
63
|
+
// ============================================================================
|
|
8
64
|
/**
|
|
9
65
|
* Auth interceptor dùng chung cho các micro-frontend.
|
|
10
66
|
* - Tự gắn header Authorization nếu có access token
|
|
@@ -15,10 +71,7 @@ export const authInterceptor = (req, next) => {
|
|
|
15
71
|
// 1. Lấy access token từ localStorage
|
|
16
72
|
const token = StorageUtil.getAccessToken();
|
|
17
73
|
let authReq = req;
|
|
18
|
-
const isExcludedRequest = req.url
|
|
19
|
-
req.url.includes("/hrm/users/register") ||
|
|
20
|
-
req.url.includes("/password-reset") ||
|
|
21
|
-
req.url.includes("/refresh");
|
|
74
|
+
const isExcludedRequest = isExcluded(req.url);
|
|
22
75
|
// 2. Gắn Authorization header nếu request không nằm trong nhóm loại trừ
|
|
23
76
|
if (token && !isExcludedRequest) {
|
|
24
77
|
authReq = req.clone({
|
|
@@ -31,19 +84,26 @@ export const authInterceptor = (req, next) => {
|
|
|
31
84
|
return next(authReq).pipe(catchError((error) => {
|
|
32
85
|
const responseBody = error.error;
|
|
33
86
|
const errorCode = responseBody?.status?.code;
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
87
|
+
// Nếu Refresh Token đã hỏng hoặc không hợp lệ -> Báo thẳng cho Shell App/Auth để Đăng xuất ngay
|
|
88
|
+
if (errorCode === ErrorCode.REFRESH_TOKEN_INVALID) {
|
|
89
|
+
window.dispatchEvent(new CustomEvent("FORCE_LOGOUT"));
|
|
90
|
+
return throwError(() => error);
|
|
91
|
+
}
|
|
92
|
+
// Token hết hạn (hoặc mã 401 chung)
|
|
93
|
+
const isAccessTokenExpired = error.status === 401 || errorCode === ErrorCode.ACCESS_TOKEN_EXPIRED;
|
|
94
|
+
// Chỉ xử lý refresh cho các request nghiệp vụ, không áp dụng cho request login/hệ thống đã cấu hình loại trừ.
|
|
95
|
+
if (isAccessTokenExpired && !isExcludedRequest) {
|
|
38
96
|
return handle401Error(authReq, next);
|
|
39
97
|
}
|
|
40
98
|
return throwError(() => error);
|
|
41
99
|
}));
|
|
42
100
|
};
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// TOKEN REFRESH LOGIC
|
|
103
|
+
// ============================================================================
|
|
43
104
|
/**
|
|
44
105
|
* Khi access token hết hạn, thư viện không tự gọi API refresh.
|
|
45
|
-
* Thay vào đó nó phát event `AUTH_TOKEN_EXPIRED` để Shell/Auth MFE chủ động refresh
|
|
46
|
-
* sau đó đợi token mới được đẩy ngược lại qua `notifyTokenRefreshed`.
|
|
106
|
+
* Thay vào đó nó phát event `AUTH_TOKEN_EXPIRED` để Shell/Auth MFE chủ động refresh.
|
|
47
107
|
*/
|
|
48
108
|
function handle401Error(request, next) {
|
|
49
109
|
if (!isRefreshing) {
|
|
@@ -53,6 +113,9 @@ function handle401Error(request, next) {
|
|
|
53
113
|
window.dispatchEvent(new CustomEvent("AUTH_TOKEN_EXPIRED"));
|
|
54
114
|
// Đợi access token mới, sau đó phát lại request cũ.
|
|
55
115
|
return refreshTokenSubject.pipe(filter((token) => token !== null), take(1), switchMap((token) => {
|
|
116
|
+
if (token === CANCEL_REFRESH_TOKEN) {
|
|
117
|
+
return throwError(() => new Error("Refresh process was cancelled. Request dropped."));
|
|
118
|
+
}
|
|
56
119
|
return next(request.clone({
|
|
57
120
|
setHeaders: { Authorization: `Bearer ${token}` },
|
|
58
121
|
}));
|
|
@@ -63,6 +126,9 @@ function handle401Error(request, next) {
|
|
|
63
126
|
else {
|
|
64
127
|
// Các request đến sau sẽ chờ cùng một đợt refresh đang diễn ra.
|
|
65
128
|
return refreshTokenSubject.pipe(filter((token) => token !== null), take(1), switchMap((token) => {
|
|
129
|
+
if (token === CANCEL_REFRESH_TOKEN) {
|
|
130
|
+
return throwError(() => new Error("Refresh process was cancelled. Request dropped."));
|
|
131
|
+
}
|
|
66
132
|
return next(request.clone({
|
|
67
133
|
setHeaders: { Authorization: `Bearer ${token}` },
|
|
68
134
|
}));
|
|
@@ -77,3 +143,12 @@ export function notifyTokenRefreshed(newToken) {
|
|
|
77
143
|
refreshTokenSubject.next(newToken);
|
|
78
144
|
isRefreshing = false;
|
|
79
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Được Shell/Auth MFE gọi khi refresh token thất bại
|
|
148
|
+
* để giải phóng toàn bộ các request đang nằm chờ trong hàng đợi
|
|
149
|
+
* tránh rò rỉ bộ nhớ (memory leak).
|
|
150
|
+
*/
|
|
151
|
+
export function cancelTokenRefresh() {
|
|
152
|
+
refreshTokenSubject.next(CANCEL_REFRESH_TOKEN);
|
|
153
|
+
isRefreshing = false;
|
|
154
|
+
}
|