@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.
@@ -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;CAC/D"}
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;AAY9B;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,iBAqC7B,CAAC;AAiDF;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAG3D"}
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.includes("/login") ||
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
- // 401 trường hợp chuẩn; ngoài ra backend thể trả REFRESH_TOKEN_INVALID.
35
- const isAuthError = error.status === 401 || errorCode === ErrorCode.REFRESH_TOKEN_INVALID;
36
- // Chỉ xử lý refresh cho các request nghiệp vụ, không áp dụng cho login/refresh.
37
- if (isAuthError && !isExcludedRequest) {
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goat-bravos/shared-lib-client",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "21.0.1",