@goat-bravos/shared-lib-client 1.0.2 → 1.0.3
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/README.md
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
# @goat-bravos/shared-lib-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Thư viện TypeScript dùng chung cho hệ sinh thái micro-frontend của InternHub. Thư viện cung cấp utility tái sử dụng, global state bằng Signals, auth interceptor và các interface API chuẩn hóa.
|
|
4
4
|
|
|
5
|
-
## 🚀
|
|
5
|
+
## 🚀 Tính năng
|
|
6
6
|
|
|
7
|
-
- 🚦 **Global Store**:
|
|
8
|
-
- 🔐 **Auth Interceptor**:
|
|
9
|
-
- 🌍 **I18n Support**:
|
|
10
|
-
- 📦 **Type-Safe API**:
|
|
11
|
-
- 💾 **Storage Utils**:
|
|
7
|
+
- 🚦 **Global Store**: Quản lý state dùng chung (User, Theme, Loading, Language) bằng Angular Signals.
|
|
8
|
+
- 🔐 **Auth Interceptor**: Tự gắn JWT vào header và đồng bộ luồng refresh token qua event.
|
|
9
|
+
- 🌍 **I18n Support**: Quản lý ngôn ngữ thống nhất và lưu bền vững trong localStorage.
|
|
10
|
+
- 📦 **Type-Safe API**: Chuẩn hóa `ResponseApi<T>` và các enum HTTP.
|
|
11
|
+
- 💾 **Storage Utils**: Wrapper an toàn kiểu dữ liệu cho `localStorage`.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
## 📦
|
|
15
|
+
## 📦 Cài đặt
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
npm install @goat-bravos/shared-lib-client
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
###
|
|
21
|
+
### Dependency đồng cấp
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Đảm bảo project đang có các dependency sau:
|
|
24
24
|
|
|
25
25
|
- `@angular/core` (>= 18.0.0)
|
|
26
26
|
- `@angular/common` (>= 18.0.0)
|
|
@@ -28,11 +28,11 @@ Ensure you have the following dependencies installed in your project:
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
-
## 🛠
|
|
31
|
+
## 🛠 Hướng dẫn sử dụng
|
|
32
32
|
|
|
33
|
-
### 1. Global Store (
|
|
33
|
+
### 1. Global Store (Quản lý state)
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
`GlobalStoreService` dùng **Angular Signals** để cung cấp state phản ứng có thể chia sẻ giữa nhiều micro-frontend.
|
|
36
36
|
|
|
37
37
|
```typescript
|
|
38
38
|
import { inject } from "@angular/core";
|
|
@@ -41,11 +41,11 @@ import { GlobalStoreService, Language } from "@goat-bravos/shared-lib-client";
|
|
|
41
41
|
export class AppSidebarComponent {
|
|
42
42
|
private globalStore = inject(GlobalStoreService);
|
|
43
43
|
|
|
44
|
-
//
|
|
44
|
+
// Đọc state
|
|
45
45
|
user = this.globalStore.user;
|
|
46
46
|
language = this.globalStore.language;
|
|
47
47
|
|
|
48
|
-
//
|
|
48
|
+
// Cập nhật state
|
|
49
49
|
changeLanguage(lang: Language) {
|
|
50
50
|
this.globalStore.setLanguage(lang);
|
|
51
51
|
}
|
|
@@ -56,30 +56,30 @@ export class AppSidebarComponent {
|
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
### 2.
|
|
59
|
+
### 2. Quốc tế hóa (i18n)
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
Việc quản lý ngôn ngữ được tích hợp sẵn trong `GlobalStoreService`.
|
|
62
62
|
|
|
63
|
-
- **
|
|
64
|
-
- **
|
|
65
|
-
-
|
|
63
|
+
- **Enum**: Dùng `Language` (`VI`, `EN`).
|
|
64
|
+
- **Khởi tạo**: Tự đọc từ `localStorage` khi service khởi tạo.
|
|
65
|
+
- **Đồng bộ**: Gọi `setLanguage()` sẽ cập nhật cả signal lẫn `localStorage`.
|
|
66
66
|
|
|
67
|
-
**
|
|
67
|
+
**Ví dụ với Ng-Zorro hoặc thư viện khác:**
|
|
68
68
|
|
|
69
69
|
```typescript
|
|
70
70
|
import { GlobalStoreService, Language } from "@goat-bravos/shared-lib-client";
|
|
71
71
|
import { en_US, vi_VN, NzI18nService } from "ng-zorro-antd/i18n";
|
|
72
72
|
|
|
73
|
-
//
|
|
74
|
-
effect(() => {
|
|
73
|
+
// Trong AppComponent hoặc component có xử lý i18n
|
|
74
|
+
effect(() => {
|
|
75
75
|
const currentLang = this.globalStore.language();
|
|
76
76
|
this.i18n.setLocale(currentLang === Language.VI ? vi_VN : en_US);
|
|
77
77
|
});
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
### 3. Authentication Interceptor
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
### 3. Bộ chặn xác thực (Authentication Interceptor)
|
|
81
|
+
|
|
82
|
+
Chuẩn hóa các API call bằng cách đăng ký `authInterceptor` trong `app.config.ts`.
|
|
83
83
|
|
|
84
84
|
```typescript
|
|
85
85
|
import { provideHttpClient, withInterceptors } from "@angular/common/http";
|
|
@@ -90,25 +90,36 @@ export const appConfig: ApplicationConfig = {
|
|
|
90
90
|
};
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
Interceptor này không tự gọi API refresh token. Khi gặp `401 Unauthorized`, nó sẽ phát event `AUTH_TOKEN_EXPIRED` để Shell/Auth MFE xử lý refresh token, sau đó chờ access token mới được đẩy lại qua `notifyTokenRefreshed(...)`.
|
|
94
|
+
|
|
95
|
+
Ví dụ ở Shell/Auth MFE:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { notifyTokenRefreshed } from "@goat-bravos/shared-lib-client";
|
|
99
|
+
|
|
100
|
+
window.addEventListener("AUTH_TOKEN_EXPIRED", async () => {
|
|
101
|
+
const newAccessToken = await refreshTokenFromApi();
|
|
102
|
+
notifyTokenRefreshed(newAccessToken);
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 4. Tiện ích Storage
|
|
107
|
+
|
|
108
|
+
Truy cập `localStorage` an toàn thông qua `StorageUtil`.
|
|
98
109
|
|
|
99
110
|
```typescript
|
|
100
111
|
import { StorageUtil, Language } from "@goat-bravos/shared-lib-client";
|
|
101
112
|
|
|
102
|
-
//
|
|
103
|
-
const token = StorageUtil.getAccessToken();
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
StorageUtil.setLanguage(Language.VI);
|
|
113
|
+
// Lấy token
|
|
114
|
+
const token = StorageUtil.getAccessToken();
|
|
115
|
+
|
|
116
|
+
// Cập nhật ngôn ngữ
|
|
117
|
+
StorageUtil.setLanguage(Language.VI);
|
|
107
118
|
```
|
|
108
119
|
|
|
109
|
-
### 5.
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
### 5. Chuẩn hóa phản hồi API
|
|
121
|
+
|
|
122
|
+
Giữ format phản hồi thống nhất giữa các backend service.
|
|
112
123
|
|
|
113
124
|
```typescript
|
|
114
125
|
import { ResponseApi, SuccessResponse } from '@goat-bravos/shared-lib-client';
|
|
@@ -118,28 +129,28 @@ interface UserData {
|
|
|
118
129
|
name: string;
|
|
119
130
|
}
|
|
120
131
|
|
|
121
|
-
//
|
|
122
|
-
getProfile(): Observable<ResponseApi<UserData>> {
|
|
132
|
+
// Trong service
|
|
133
|
+
getProfile(): Observable<ResponseApi<UserData>> {
|
|
123
134
|
return this.http.get<ResponseApi<UserData>>('/api/profile');
|
|
124
135
|
}
|
|
125
136
|
```
|
|
126
137
|
|
|
127
138
|
---
|
|
128
139
|
|
|
129
|
-
## 📂
|
|
140
|
+
## 📂 Cấu trúc thư mục
|
|
130
141
|
|
|
131
142
|
```text
|
|
132
143
|
src/
|
|
133
|
-
├── enums/ #
|
|
134
|
-
├── interceptors/ #
|
|
135
|
-
├── interfaces/ #
|
|
136
|
-
├── store/ # Global Signals
|
|
137
|
-
├── utils/ #
|
|
138
|
-
└── index.ts #
|
|
144
|
+
├── enums/ # Các enum mã HTTP, mã lỗi, khóa localStorage, ngôn ngữ
|
|
145
|
+
├── interceptors/ # Các interceptor dùng chung cho auth và lỗi
|
|
146
|
+
├── interfaces/ # Các interface chuẩn hóa cho response API và phân trang
|
|
147
|
+
├── store/ # Global store dùng Angular Signals (singleton)
|
|
148
|
+
├── utils/ # Các utility cho storage và helper
|
|
149
|
+
└── index.ts # Điểm export public của thư viện
|
|
139
150
|
```
|
|
140
151
|
|
|
141
152
|
---
|
|
142
153
|
|
|
143
|
-
## 🛡️
|
|
154
|
+
## 🛡️ Giấy phép
|
|
144
155
|
|
|
145
156
|
MIT
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { HttpInterceptorFn } from "@angular/common/http";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
3
|
+
* Auth interceptor dùng chung cho các micro-frontend.
|
|
4
|
+
* - Tự gắn header Authorization nếu có access token
|
|
5
|
+
* - Khi gặp lỗi 401 thì phát event để Shell/Auth xử lý refresh token
|
|
6
|
+
* - Các request đang chờ sẽ được đồng bộ qua `notifyTokenRefreshed`
|
|
6
7
|
*/
|
|
7
8
|
export declare const authInterceptor: HttpInterceptorFn;
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* Được Shell/Auth MFE gọi sau khi refresh token thành công
|
|
11
|
+
* để đánh thức các request đang chờ.
|
|
10
12
|
*/
|
|
11
13
|
export declare function notifyTokenRefreshed(newToken: string): void;
|
|
12
14
|
//# 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
|
|
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"}
|
|
@@ -2,56 +2,56 @@ 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
|
-
//
|
|
5
|
+
// Cờ và subject dùng để đồng bộ các request khi access token hết hạn.
|
|
6
6
|
let isRefreshing = false;
|
|
7
7
|
const refreshTokenSubject = new BehaviorSubject(null);
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
9
|
+
* Auth interceptor dùng chung cho các micro-frontend.
|
|
10
|
+
* - Tự gắn header Authorization nếu có access token
|
|
11
|
+
* - Khi gặp lỗi 401 thì phát event để Shell/Auth xử lý refresh token
|
|
12
|
+
* - Các request đang chờ sẽ được đồng bộ qua `notifyTokenRefreshed`
|
|
12
13
|
*/
|
|
13
14
|
export const authInterceptor = (req, next) => {
|
|
14
|
-
// 1.
|
|
15
|
+
// 1. Lấy access token từ localStorage
|
|
15
16
|
const token = StorageUtil.getAccessToken();
|
|
16
17
|
let authReq = req;
|
|
17
|
-
|
|
18
|
-
|
|
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");
|
|
22
|
+
// 2. Gắn Authorization header nếu request không nằm trong nhóm loại trừ
|
|
23
|
+
if (token && !isExcludedRequest) {
|
|
19
24
|
authReq = req.clone({
|
|
20
25
|
setHeaders: {
|
|
21
26
|
Authorization: `Bearer ${token}`,
|
|
22
27
|
},
|
|
23
28
|
});
|
|
24
29
|
}
|
|
25
|
-
// 3.
|
|
30
|
+
// 3. Thực thi request và xử lý lỗi xác thực nếu có
|
|
26
31
|
return next(authReq).pipe(catchError((error) => {
|
|
27
32
|
const responseBody = error.error;
|
|
28
33
|
const errorCode = responseBody?.status?.code;
|
|
29
|
-
//
|
|
30
|
-
// 401 is standard, but the backend also uses REFRESH_TOKEN_INVALID in some contexts
|
|
34
|
+
// 401 là trường hợp chuẩn; ngoài ra backend có thể trả mã REFRESH_TOKEN_INVALID.
|
|
31
35
|
const isAuthError = error.status === 401 || errorCode === ErrorCode.REFRESH_TOKEN_INVALID;
|
|
32
|
-
//
|
|
33
|
-
if (isAuthError &&
|
|
34
|
-
!authReq.url.includes("/auth/login") &&
|
|
35
|
-
!authReq.url.includes("/auth/refresh")) {
|
|
36
|
+
// Chỉ xử lý refresh cho các request nghiệp vụ, không áp dụng cho login/refresh.
|
|
37
|
+
if (isAuthError && !isExcludedRequest) {
|
|
36
38
|
return handle401Error(authReq, next);
|
|
37
39
|
}
|
|
38
40
|
return throwError(() => error);
|
|
39
41
|
}));
|
|
40
42
|
};
|
|
41
43
|
/**
|
|
42
|
-
*
|
|
44
|
+
* 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`.
|
|
43
47
|
*/
|
|
44
48
|
function handle401Error(request, next) {
|
|
45
49
|
if (!isRefreshing) {
|
|
46
50
|
isRefreshing = true;
|
|
47
51
|
refreshTokenSubject.next(null);
|
|
48
|
-
|
|
49
|
-
// Here we assume the Shell/App environment has a way to call refresh token
|
|
50
|
-
// For a library, we might need to pass a callback or use a known endpoint
|
|
51
|
-
// Let's assume a standard endpoint or dispatch a CustomEvent for the Shell to handle
|
|
52
|
-
// Dispatch event so the Shell/Auth MFE can perform the actual API call
|
|
52
|
+
// Phát tín hiệu để Shell/Auth MFE thực hiện API refresh token thật sự.
|
|
53
53
|
window.dispatchEvent(new CustomEvent("AUTH_TOKEN_EXPIRED"));
|
|
54
|
-
//
|
|
54
|
+
// Đợi access token mới, sau đó phát lại request cũ.
|
|
55
55
|
return refreshTokenSubject.pipe(filter((token) => token !== null), take(1), switchMap((token) => {
|
|
56
56
|
return next(request.clone({
|
|
57
57
|
setHeaders: { Authorization: `Bearer ${token}` },
|
|
@@ -61,7 +61,7 @@ function handle401Error(request, next) {
|
|
|
61
61
|
}));
|
|
62
62
|
}
|
|
63
63
|
else {
|
|
64
|
-
//
|
|
64
|
+
// Các request đến sau sẽ chờ cùng một đợt refresh đang diễn ra.
|
|
65
65
|
return refreshTokenSubject.pipe(filter((token) => token !== null), take(1), switchMap((token) => {
|
|
66
66
|
return next(request.clone({
|
|
67
67
|
setHeaders: { Authorization: `Bearer ${token}` },
|
|
@@ -70,7 +70,8 @@ function handle401Error(request, next) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
73
|
+
* Được Shell/Auth MFE gọi sau khi refresh token thành công
|
|
74
|
+
* để đánh thức các request đang chờ.
|
|
74
75
|
*/
|
|
75
76
|
export function notifyTokenRefreshed(newToken) {
|
|
76
77
|
refreshTokenSubject.next(newToken);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goat-bravos/shared-lib-client",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"peerDependencies": {
|
|
6
6
|
"@angular/common": "21.0.1",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"dist",
|
|
25
25
|
"README.md"
|
|
26
26
|
],
|
|
27
|
-
"scripts": {
|
|
28
|
-
"build": "tsc",
|
|
29
|
-
"prepublishOnly": "npm run build",
|
|
30
|
-
"publish:lib": "npm --cache .npm-cache publish"
|
|
31
|
-
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"prepublishOnly": "npm run build",
|
|
30
|
+
"publish:lib": "npm --cache .npm-cache publish"
|
|
31
|
+
},
|
|
32
32
|
"keywords": [
|
|
33
33
|
"typescript",
|
|
34
34
|
"api",
|