@warriorteam/redai-zalo-sdk 1.2.0 → 1.4.0
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 +563 -550
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -5
- package/dist/index.js.map +1 -1
- package/dist/services/article.service.d.ts +1 -1
- package/dist/services/article.service.d.ts.map +1 -1
- package/dist/services/article.service.js +24 -16
- package/dist/services/article.service.js.map +1 -1
- package/dist/services/auth.service.d.ts +1 -0
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +23 -9
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/consultation.service.d.ts +63 -16
- package/dist/services/consultation.service.d.ts.map +1 -1
- package/dist/services/consultation.service.js +264 -49
- package/dist/services/consultation.service.js.map +1 -1
- package/dist/services/general-message.service.d.ts +2 -25
- package/dist/services/general-message.service.d.ts.map +1 -1
- package/dist/services/general-message.service.js +11 -112
- package/dist/services/general-message.service.js.map +1 -1
- package/dist/services/group-management.service.d.ts +1 -1
- package/dist/services/group-management.service.d.ts.map +1 -1
- package/dist/services/group-management.service.js +59 -27
- package/dist/services/group-management.service.js.map +1 -1
- package/dist/services/group-message.service.d.ts +1 -1
- package/dist/services/group-message.service.d.ts.map +1 -1
- package/dist/services/group-message.service.js +49 -23
- package/dist/services/group-message.service.js.map +1 -1
- package/dist/services/message-management.service.d.ts +21 -2
- package/dist/services/message-management.service.d.ts.map +1 -1
- package/dist/services/message-management.service.js +83 -7
- package/dist/services/message-management.service.js.map +1 -1
- package/dist/services/oa.service.d.ts +1 -0
- package/dist/services/oa.service.d.ts.map +1 -1
- package/dist/services/oa.service.js +23 -5
- package/dist/services/oa.service.js.map +1 -1
- package/dist/services/user.service.d.ts +108 -18
- package/dist/services/user.service.d.ts.map +1 -1
- package/dist/services/user.service.js +260 -59
- package/dist/services/user.service.js.map +1 -1
- package/dist/services/video-upload.service.d.ts +1 -1
- package/dist/services/video-upload.service.d.ts.map +1 -1
- package/dist/services/video-upload.service.js +11 -8
- package/dist/services/video-upload.service.js.map +1 -1
- package/dist/services/zns.service.d.ts +88 -21
- package/dist/services/zns.service.d.ts.map +1 -1
- package/dist/services/zns.service.js +157 -42
- package/dist/services/zns.service.js.map +1 -1
- package/dist/types/group.d.ts +5 -5
- package/dist/types/group.d.ts.map +1 -1
- package/dist/types/user.d.ts +155 -12
- package/dist/types/user.d.ts.map +1 -1
- package/dist/types/user.js.map +1 -1
- package/dist/types/zns.d.ts +67 -33
- package/dist/types/zns.d.ts.map +1 -1
- package/dist/zalo-sdk.d.ts +3 -11
- package/dist/zalo-sdk.d.ts.map +1 -1
- package/dist/zalo-sdk.js +0 -10
- package/dist/zalo-sdk.js.map +1 -1
- package/docs/API_REFERENCE.md +680 -0
- package/docs/AUTHENTICATION.md +709 -0
- package/docs/CONSULTATION_SERVICE.md +512 -330
- package/docs/GROUP_MANAGEMENT.md +2 -2
- package/docs/MESSAGE_SERVICES.md +1224 -0
- package/docs/TAG_MANAGEMENT.md +1462 -0
- package/docs/USER_MANAGEMENT.md +481 -0
- package/docs/ZNS_SERVICE.md +985 -0
- package/package.json +1 -1
- package/dist/services/tag.service.d.ts +0 -144
- package/dist/services/tag.service.d.ts.map +0 -1
- package/dist/services/tag.service.js +0 -184
- package/dist/services/tag.service.js.map +0 -1
- package/dist/services/user-management.service.d.ts +0 -117
- package/dist/services/user-management.service.d.ts.map +0 -1
- package/dist/services/user-management.service.js +0 -239
- package/dist/services/user-management.service.js.map +0 -1
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
# RedAI Zalo SDK - Authentication Guide
|
|
2
|
+
|
|
3
|
+
## Tổng quan
|
|
4
|
+
|
|
5
|
+
RedAI Zalo SDK hỗ trợ đầy đủ các authentication flows của Zalo, bao gồm:
|
|
6
|
+
|
|
7
|
+
- **Official Account (OA) Authentication** - Để truy cập OA APIs
|
|
8
|
+
- **Social API Authentication** - Để truy cập thông tin user social
|
|
9
|
+
- **Token Management** - Refresh và validate tokens
|
|
10
|
+
- **PKCE Support** - Security enhancement cho Social API
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Official Account Authentication
|
|
15
|
+
|
|
16
|
+
### 1. Tạo Authorization URL
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { ZaloSDK } from "@warriorteam/redai-zalo-sdk";
|
|
20
|
+
|
|
21
|
+
const zalo = new ZaloSDK({
|
|
22
|
+
appId: "your-oa-app-id",
|
|
23
|
+
appSecret: "your-oa-app-secret"
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Tạo authorization URL cho OA
|
|
27
|
+
const authUrl = zalo.createOAAuthUrl(
|
|
28
|
+
"https://your-app.com/auth/callback", // redirect_uri
|
|
29
|
+
"optional-state-parameter" // state (tùy chọn)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
console.log("Redirect user to:", authUrl);
|
|
33
|
+
// Output: https://oauth.zaloapp.com/v4/oa/permission?app_id=xxx&redirect_uri=xxx&state=xxx
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Xử lý Callback và Lấy Access Token
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// Trong route callback của bạn
|
|
40
|
+
app.get('/auth/callback', async (req, res) => {
|
|
41
|
+
const { code, state } = req.query;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Lấy access token từ authorization code
|
|
45
|
+
const tokenResponse = await zalo.getOAAccessToken(
|
|
46
|
+
code as string,
|
|
47
|
+
"https://your-app.com/auth/callback"
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
console.log("OA Access Token:", tokenResponse.access_token);
|
|
51
|
+
console.log("Refresh Token:", tokenResponse.refresh_token);
|
|
52
|
+
console.log("Expires In:", tokenResponse.expires_in); // seconds
|
|
53
|
+
|
|
54
|
+
// Lưu tokens vào database/session
|
|
55
|
+
await saveTokens(tokenResponse);
|
|
56
|
+
|
|
57
|
+
res.redirect('/dashboard');
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("Auth error:", error);
|
|
60
|
+
res.redirect('/auth/error');
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 3. Token Response Structure
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
interface AccessToken {
|
|
69
|
+
access_token: string; // Token để gọi API
|
|
70
|
+
refresh_token: string; // Token để refresh
|
|
71
|
+
expires_in: number; // Thời gian sống (seconds)
|
|
72
|
+
token_type: "Bearer"; // Loại token
|
|
73
|
+
scope: string; // Quyền được cấp
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 4. Sử dụng Access Token
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// Lấy thông tin OA
|
|
81
|
+
const oaInfo = await zalo.getOAInfo(tokenResponse.access_token);
|
|
82
|
+
console.log("OA Name:", oaInfo.name);
|
|
83
|
+
console.log("Followers:", oaInfo.num_follower);
|
|
84
|
+
|
|
85
|
+
// Gửi tin nhắn consultation
|
|
86
|
+
await zalo.sendConsultationText(
|
|
87
|
+
tokenResponse.access_token,
|
|
88
|
+
"user-zalo-id",
|
|
89
|
+
"Xin chào! Cảm ơn bạn đã quan tâm đến dịch vụ của chúng tôi."
|
|
90
|
+
);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Social API Authentication
|
|
96
|
+
|
|
97
|
+
### 1. Tạo Authorization URL (Basic)
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const zalo = new ZaloSDK({
|
|
101
|
+
appId: "your-social-app-id",
|
|
102
|
+
appSecret: "your-social-app-secret"
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Tạo authorization URL cho Social API
|
|
106
|
+
const authUrl = zalo.createSocialAuthUrl(
|
|
107
|
+
"https://your-app.com/social/callback",
|
|
108
|
+
"optional-state"
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
console.log("Redirect user to:", authUrl);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 2. Tạo Authorization URL với PKCE (Khuyến nghị)
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Tạo PKCE parameters cho bảo mật cao hơn
|
|
118
|
+
const pkce = zalo.generatePKCE();
|
|
119
|
+
console.log("Code Verifier:", pkce.code_verifier);
|
|
120
|
+
console.log("Code Challenge:", pkce.code_challenge);
|
|
121
|
+
|
|
122
|
+
// Lưu code_verifier vào session/state
|
|
123
|
+
req.session.codeVerifier = pkce.code_verifier;
|
|
124
|
+
|
|
125
|
+
const authUrl = zalo.createSocialAuthUrl(
|
|
126
|
+
"https://your-app.com/social/callback",
|
|
127
|
+
"social-login"
|
|
128
|
+
);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 3. PKCE Generation
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
interface PKCEConfig {
|
|
135
|
+
code_verifier: string; // Random string 43-128 chars
|
|
136
|
+
code_challenge: string; // base64url(sha256(code_verifier))
|
|
137
|
+
code_challenge_method: "S256";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// SDK tự động tạo PKCE theo chuẩn RFC 7636
|
|
141
|
+
const pkce = zalo.generatePKCE();
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 4. Xử lý Social Callback
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
app.get('/social/callback', async (req, res) => {
|
|
148
|
+
const { code, state } = req.query;
|
|
149
|
+
const codeVerifier = req.session.codeVerifier; // Lấy từ session
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const tokenResponse = await zalo.getSocialAccessToken(
|
|
153
|
+
code as string,
|
|
154
|
+
"https://your-app.com/social/callback",
|
|
155
|
+
codeVerifier // Bắt buộc nếu dùng PKCE
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Lấy thông tin user
|
|
159
|
+
const userInfo = await zalo.getSocialUserInfo(
|
|
160
|
+
tokenResponse.access_token,
|
|
161
|
+
"id,name,picture,birthday,gender" // fields cần lấy
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
console.log("User Info:", userInfo);
|
|
165
|
+
|
|
166
|
+
// Lưu vào database
|
|
167
|
+
await createOrUpdateUser(userInfo, tokenResponse);
|
|
168
|
+
|
|
169
|
+
res.redirect('/profile');
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error("Social auth error:", error);
|
|
172
|
+
res.redirect('/login?error=auth_failed');
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 5. Social User Info Response
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
interface SocialUserInfo {
|
|
181
|
+
id: string; // Zalo user ID
|
|
182
|
+
name: string; // Tên hiển thị
|
|
183
|
+
picture?: {
|
|
184
|
+
data: {
|
|
185
|
+
url: string; // Avatar URL
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
birthday?: string; // Ngày sinh (YYYY-MM-DD)
|
|
189
|
+
gender?: number; // 1: Nam, 2: Nữ
|
|
190
|
+
locale?: string; // Locale
|
|
191
|
+
// Các fields khác tùy theo quyền được cấp
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Token Management
|
|
198
|
+
|
|
199
|
+
### 1. Refresh OA Access Token
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
async function refreshOAToken(refreshToken: string): Promise<AccessToken> {
|
|
203
|
+
try {
|
|
204
|
+
const newTokens = await zalo.refreshOAAccessToken(refreshToken);
|
|
205
|
+
|
|
206
|
+
// Lưu tokens mới
|
|
207
|
+
await updateTokens(newTokens);
|
|
208
|
+
|
|
209
|
+
return newTokens;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error("Failed to refresh OA token:", error);
|
|
212
|
+
// Redirect to re-authentication
|
|
213
|
+
throw new Error("Re-authentication required");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 2. Refresh Social Access Token
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
async function refreshSocialToken(refreshToken: string): Promise<AccessToken> {
|
|
222
|
+
try {
|
|
223
|
+
const newTokens = await zalo.refreshSocialAccessToken(refreshToken);
|
|
224
|
+
return newTokens;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error("Failed to refresh social token:", error);
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### 3. Auto Token Refresh Middleware
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// Express middleware để auto refresh tokens
|
|
236
|
+
export const autoRefreshToken = async (req: Request, res: Response, next: NextFunction) => {
|
|
237
|
+
const { accessToken, refreshToken, expiresAt } = req.user;
|
|
238
|
+
|
|
239
|
+
// Kiểm tra token sắp hết hạn (trước 5 phút)
|
|
240
|
+
const willExpireSoon = Date.now() > (expiresAt - 5 * 60 * 1000);
|
|
241
|
+
|
|
242
|
+
if (willExpireSoon && refreshToken) {
|
|
243
|
+
try {
|
|
244
|
+
const tokenType = req.path.includes('/oa/') ? 'oa' : 'social';
|
|
245
|
+
|
|
246
|
+
const newTokens = tokenType === 'oa'
|
|
247
|
+
? await zalo.refreshOAAccessToken(refreshToken)
|
|
248
|
+
: await zalo.refreshSocialAccessToken(refreshToken);
|
|
249
|
+
|
|
250
|
+
// Cập nhật user với tokens mới
|
|
251
|
+
req.user.accessToken = newTokens.access_token;
|
|
252
|
+
req.user.refreshToken = newTokens.refresh_token;
|
|
253
|
+
req.user.expiresAt = Date.now() + (newTokens.expires_in * 1000);
|
|
254
|
+
|
|
255
|
+
// Lưu vào database
|
|
256
|
+
await updateUserTokens(req.user.id, newTokens);
|
|
257
|
+
|
|
258
|
+
console.log("Token refreshed successfully");
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error("Auto refresh failed:", error);
|
|
261
|
+
return res.status(401).json({ error: "Authentication expired" });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
next();
|
|
266
|
+
};
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Token Validation
|
|
272
|
+
|
|
273
|
+
### 1. Validate Access Token
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// Validate OA token
|
|
277
|
+
const isValidOA = await zalo.validateAccessToken(accessToken, 'oa');
|
|
278
|
+
console.log("OA Token valid:", isValidOA);
|
|
279
|
+
|
|
280
|
+
// Validate Social token
|
|
281
|
+
const isValidSocial = await zalo.validateAccessToken(accessToken, 'social');
|
|
282
|
+
console.log("Social Token valid:", isValidSocial);
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### 2. Advanced Token Validation
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// Sử dụng service trực tiếp để có thêm thông tin
|
|
289
|
+
const validation = await zalo.auth.validateAccessToken(accessToken);
|
|
290
|
+
|
|
291
|
+
interface TokenValidation {
|
|
292
|
+
valid: boolean;
|
|
293
|
+
expires_in?: number; // Thời gian còn lại (seconds)
|
|
294
|
+
scope?: string; // Quyền hiện tại
|
|
295
|
+
app_id?: string; // App ID của token
|
|
296
|
+
user_id?: string; // User ID (nếu có)
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Security Best Practices
|
|
303
|
+
|
|
304
|
+
### 1. State Parameter
|
|
305
|
+
|
|
306
|
+
Luôn sử dụng `state` parameter để chống CSRF:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
// Tạo random state
|
|
310
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
311
|
+
req.session.authState = state;
|
|
312
|
+
|
|
313
|
+
const authUrl = zalo.createOAAuthUrl(redirectUri, state);
|
|
314
|
+
|
|
315
|
+
// Trong callback, verify state
|
|
316
|
+
if (req.query.state !== req.session.authState) {
|
|
317
|
+
throw new Error("Invalid state parameter");
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### 2. PKCE cho Social API
|
|
322
|
+
|
|
323
|
+
Luôn sử dụng PKCE cho Social API:
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
// ✅ Đúng - với PKCE
|
|
327
|
+
const pkce = zalo.generatePKCE();
|
|
328
|
+
req.session.codeVerifier = pkce.code_verifier;
|
|
329
|
+
|
|
330
|
+
// ❌ Sai - không dùng PKCE
|
|
331
|
+
const authUrl = zalo.createSocialAuthUrl(redirectUri);
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### 3. Token Storage
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// ✅ Đúng - mã hóa tokens khi lưu
|
|
338
|
+
const encryptedToken = encrypt(accessToken);
|
|
339
|
+
await db.users.update(userId, {
|
|
340
|
+
access_token: encryptedToken,
|
|
341
|
+
refresh_token: encrypt(refreshToken)
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// ❌ Sai - lưu plain text
|
|
345
|
+
await db.users.update(userId, {
|
|
346
|
+
access_token: accessToken // Không an toàn
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### 4. Token Scope Validation
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
// Kiểm tra token có đúng scope không
|
|
354
|
+
async function requireScope(requiredScope: string) {
|
|
355
|
+
const validation = await zalo.auth.validateAccessToken(accessToken);
|
|
356
|
+
|
|
357
|
+
if (!validation.scope?.includes(requiredScope)) {
|
|
358
|
+
throw new Error(`Missing required scope: ${requiredScope}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Usage
|
|
363
|
+
await requireScope('oa.message.send');
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Error Handling
|
|
369
|
+
|
|
370
|
+
### 1. Common Auth Errors
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
try {
|
|
374
|
+
const tokens = await zalo.getOAAccessToken(code, redirectUri);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (error.code === -201) {
|
|
377
|
+
// Invalid parameters (code expired, wrong redirect_uri, etc.)
|
|
378
|
+
console.error("Invalid auth parameters:", error.message);
|
|
379
|
+
} else if (error.code === -216) {
|
|
380
|
+
// Invalid app credentials
|
|
381
|
+
console.error("Invalid app_id or app_secret");
|
|
382
|
+
} else {
|
|
383
|
+
console.error("Unexpected auth error:", error);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### 2. Token Refresh Errors
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
async function handleTokenRefresh(refreshToken: string) {
|
|
392
|
+
try {
|
|
393
|
+
return await zalo.refreshOAAccessToken(refreshToken);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (error.code === -216) {
|
|
396
|
+
// Refresh token expired/invalid
|
|
397
|
+
console.log("Refresh token expired, require re-authentication");
|
|
398
|
+
redirectToLogin();
|
|
399
|
+
} else {
|
|
400
|
+
console.error("Refresh failed:", error);
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Complete Authentication Flow Examples
|
|
410
|
+
|
|
411
|
+
### 1. OA Authentication với Express
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
import express from 'express';
|
|
415
|
+
import { ZaloSDK } from '@warriorteam/redai-zalo-sdk';
|
|
416
|
+
|
|
417
|
+
const app = express();
|
|
418
|
+
const zalo = new ZaloSDK({
|
|
419
|
+
appId: process.env.ZALO_OA_APP_ID!,
|
|
420
|
+
appSecret: process.env.ZALO_OA_APP_SECRET!,
|
|
421
|
+
debug: process.env.NODE_ENV === 'development'
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Route bắt đầu auth
|
|
425
|
+
app.get('/auth/oa', (req, res) => {
|
|
426
|
+
const state = generateRandomState();
|
|
427
|
+
req.session.authState = state;
|
|
428
|
+
|
|
429
|
+
const authUrl = zalo.createOAAuthUrl(
|
|
430
|
+
`${process.env.BASE_URL}/auth/oa/callback`,
|
|
431
|
+
state
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
res.redirect(authUrl);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Callback handler
|
|
438
|
+
app.get('/auth/oa/callback', async (req, res) => {
|
|
439
|
+
try {
|
|
440
|
+
const { code, state, error } = req.query;
|
|
441
|
+
|
|
442
|
+
if (error) {
|
|
443
|
+
return res.redirect(`/auth/error?reason=${error}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (state !== req.session.authState) {
|
|
447
|
+
return res.status(400).json({ error: 'Invalid state' });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const tokens = await zalo.getOAAccessToken(
|
|
451
|
+
code as string,
|
|
452
|
+
`${process.env.BASE_URL}/auth/oa/callback`
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
// Lấy thông tin OA
|
|
456
|
+
const oaInfo = await zalo.getOAInfo(tokens.access_token);
|
|
457
|
+
|
|
458
|
+
// Lưu vào database
|
|
459
|
+
const oaAccount = await OAAccount.create({
|
|
460
|
+
oa_id: oaInfo.oa_id,
|
|
461
|
+
name: oaInfo.name,
|
|
462
|
+
access_token: encrypt(tokens.access_token),
|
|
463
|
+
refresh_token: encrypt(tokens.refresh_token),
|
|
464
|
+
expires_at: new Date(Date.now() + tokens.expires_in * 1000),
|
|
465
|
+
scope: tokens.scope
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
req.session.oaId = oaAccount.id;
|
|
469
|
+
res.redirect('/oa/dashboard');
|
|
470
|
+
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.error('OA Auth error:', error);
|
|
473
|
+
res.redirect('/auth/error');
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### 2. Social Authentication với Next.js
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
// pages/api/auth/social/login.ts
|
|
482
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
483
|
+
import { ZaloSDK } from '@warriorteam/redai-zalo-sdk';
|
|
484
|
+
|
|
485
|
+
const zalo = new ZaloSDK({
|
|
486
|
+
appId: process.env.ZALO_SOCIAL_APP_ID!,
|
|
487
|
+
appSecret: process.env.ZALO_SOCIAL_APP_SECRET!
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
491
|
+
if (req.method !== 'GET') {
|
|
492
|
+
return res.status(405).json({ error: 'Method not allowed' });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Generate PKCE
|
|
496
|
+
const pkce = zalo.generatePKCE();
|
|
497
|
+
const state = generateRandomState();
|
|
498
|
+
|
|
499
|
+
// Lưu vào session/cookie (hoặc Redis)
|
|
500
|
+
res.setHeader('Set-Cookie', [
|
|
501
|
+
`pkce_verifier=${pkce.code_verifier}; HttpOnly; Secure; SameSite=Strict`,
|
|
502
|
+
`auth_state=${state}; HttpOnly; Secure; SameSite=Strict`
|
|
503
|
+
]);
|
|
504
|
+
|
|
505
|
+
const authUrl = zalo.createSocialAuthUrl(
|
|
506
|
+
`${process.env.NEXTAUTH_URL}/api/auth/social/callback`,
|
|
507
|
+
state
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
res.redirect(authUrl);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// pages/api/auth/social/callback.ts
|
|
514
|
+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
515
|
+
try {
|
|
516
|
+
const { code, state } = req.query;
|
|
517
|
+
const cookies = parseCookies(req.headers.cookie || '');
|
|
518
|
+
|
|
519
|
+
if (state !== cookies.auth_state) {
|
|
520
|
+
throw new Error('Invalid state');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const tokens = await zalo.getSocialAccessToken(
|
|
524
|
+
code as string,
|
|
525
|
+
`${process.env.NEXTAUTH_URL}/api/auth/social/callback`,
|
|
526
|
+
cookies.pkce_verifier
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
const userInfo = await zalo.getSocialUserInfo(
|
|
530
|
+
tokens.access_token,
|
|
531
|
+
'id,name,picture,birthday'
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
// Tạo hoặc cập nhật user
|
|
535
|
+
const user = await User.upsert({
|
|
536
|
+
zalo_id: userInfo.id,
|
|
537
|
+
name: userInfo.name,
|
|
538
|
+
avatar: userInfo.picture?.data.url,
|
|
539
|
+
birthday: userInfo.birthday
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// Lưu tokens
|
|
543
|
+
await UserToken.create({
|
|
544
|
+
user_id: user.id,
|
|
545
|
+
access_token: encrypt(tokens.access_token),
|
|
546
|
+
refresh_token: encrypt(tokens.refresh_token),
|
|
547
|
+
expires_at: new Date(Date.now() + tokens.expires_in * 1000),
|
|
548
|
+
token_type: 'social'
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Tạo session
|
|
552
|
+
const sessionToken = jwt.sign(
|
|
553
|
+
{ userId: user.id, zaloId: userInfo.id },
|
|
554
|
+
process.env.JWT_SECRET!,
|
|
555
|
+
{ expiresIn: '7d' }
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
res.setHeader('Set-Cookie', `session=${sessionToken}; HttpOnly; Secure`);
|
|
559
|
+
res.redirect('/profile');
|
|
560
|
+
|
|
561
|
+
} catch (error) {
|
|
562
|
+
console.error('Social auth error:', error);
|
|
563
|
+
res.redirect('/login?error=auth_failed');
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## Environment Configuration
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
// .env file
|
|
574
|
+
ZALO_OA_APP_ID=your_oa_app_id
|
|
575
|
+
ZALO_OA_APP_SECRET=your_oa_app_secret
|
|
576
|
+
ZALO_SOCIAL_APP_ID=your_social_app_id
|
|
577
|
+
ZALO_SOCIAL_APP_SECRET=your_social_app_secret
|
|
578
|
+
|
|
579
|
+
// config.ts
|
|
580
|
+
export const zaloConfig = {
|
|
581
|
+
oa: {
|
|
582
|
+
appId: process.env.ZALO_OA_APP_ID!,
|
|
583
|
+
appSecret: process.env.ZALO_OA_APP_SECRET!,
|
|
584
|
+
},
|
|
585
|
+
social: {
|
|
586
|
+
appId: process.env.ZALO_SOCIAL_APP_ID!,
|
|
587
|
+
appSecret: process.env.ZALO_SOCIAL_APP_SECRET!,
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
// Khởi tạo SDK instances
|
|
592
|
+
export const zaloOA = new ZaloSDK(zaloConfig.oa);
|
|
593
|
+
export const zaloSocial = new ZaloSDK(zaloConfig.social);
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
## Testing Authentication
|
|
599
|
+
|
|
600
|
+
### 1. Unit Tests
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
// auth.test.ts
|
|
604
|
+
import { ZaloSDK } from '@warriorteam/redai-zalo-sdk';
|
|
605
|
+
|
|
606
|
+
describe('Authentication', () => {
|
|
607
|
+
const zalo = new ZaloSDK({
|
|
608
|
+
appId: 'test_app_id',
|
|
609
|
+
appSecret: 'test_app_secret'
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('should create OA auth URL', () => {
|
|
613
|
+
const url = zalo.createOAAuthUrl('http://localhost:3000/callback', 'test-state');
|
|
614
|
+
expect(url).toContain('oauth.zaloapp.com/v4/oa/permission');
|
|
615
|
+
expect(url).toContain('app_id=test_app_id');
|
|
616
|
+
expect(url).toContain('state=test-state');
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('should generate valid PKCE', () => {
|
|
620
|
+
const pkce = zalo.generatePKCE();
|
|
621
|
+
expect(pkce.code_verifier).toHaveLength(128);
|
|
622
|
+
expect(pkce.code_challenge_method).toBe('S256');
|
|
623
|
+
expect(pkce.code_challenge).toBeDefined();
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### 2. Integration Tests
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
// auth.integration.test.ts
|
|
632
|
+
describe('Authentication Integration', () => {
|
|
633
|
+
it('should complete OA auth flow', async () => {
|
|
634
|
+
// Sử dụng test credentials
|
|
635
|
+
const testCode = 'test_authorization_code';
|
|
636
|
+
const testRedirectUri = 'http://localhost:3000/callback';
|
|
637
|
+
|
|
638
|
+
const tokens = await zalo.getOAAccessToken(testCode, testRedirectUri);
|
|
639
|
+
|
|
640
|
+
expect(tokens.access_token).toBeDefined();
|
|
641
|
+
expect(tokens.refresh_token).toBeDefined();
|
|
642
|
+
expect(tokens.expires_in).toBeGreaterThan(0);
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
## Troubleshooting
|
|
650
|
+
|
|
651
|
+
### 1. Common Issues
|
|
652
|
+
|
|
653
|
+
**Q: "Invalid redirect_uri" error**
|
|
654
|
+
```
|
|
655
|
+
A: Đảm bảo redirect_uri trong callback chính xác giống với lúc tạo auth URL
|
|
656
|
+
và đã được đăng ký trong Zalo Developer Console
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
**Q: "Invalid code" error**
|
|
660
|
+
```
|
|
661
|
+
A: Authorization code chỉ dùng được 1 lần và có thời gian sống ngắn (~10 phút)
|
|
662
|
+
Đảm bảo xử lý callback ngay sau khi user authorize
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
**Q: "Invalid app_id or app_secret"**
|
|
666
|
+
```
|
|
667
|
+
A: Kiểm tra credentials trong Zalo Developer Console
|
|
668
|
+
Đảm bảo dùng đúng app_id/app_secret cho môi trường (dev/prod)
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### 2. Debug Authentication
|
|
672
|
+
|
|
673
|
+
```typescript
|
|
674
|
+
// Enable debug logging
|
|
675
|
+
const zalo = new ZaloSDK({
|
|
676
|
+
appId: 'your_app_id',
|
|
677
|
+
appSecret: 'your_app_secret',
|
|
678
|
+
debug: true // Bật debug logs
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// Manual token validation
|
|
682
|
+
async function debugToken(accessToken: string) {
|
|
683
|
+
try {
|
|
684
|
+
const validation = await zalo.auth.validateAccessToken(accessToken);
|
|
685
|
+
console.log('Token validation:', validation);
|
|
686
|
+
|
|
687
|
+
if (validation.valid) {
|
|
688
|
+
console.log(`Token expires in: ${validation.expires_in} seconds`);
|
|
689
|
+
console.log(`Token scope: ${validation.scope}`);
|
|
690
|
+
}
|
|
691
|
+
} catch (error) {
|
|
692
|
+
console.error('Token validation failed:', error);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
## Next Steps
|
|
700
|
+
|
|
701
|
+
Sau khi hoàn thành authentication:
|
|
702
|
+
|
|
703
|
+
1. **[ZNS Service](./ZNS_SERVICE.md)** - Gửi notification messages
|
|
704
|
+
2. **[Message Services](./MESSAGE_SERVICES.md)** - Gửi các loại tin nhắn khác nhau
|
|
705
|
+
3. **[User Management](./USER_MANAGEMENT.md)** - Quản lý user profiles
|
|
706
|
+
4. **[Group Management](./GROUP_MANAGEMENT.md)** - Quản lý Zalo groups
|
|
707
|
+
5. **[Webhook Integration](./WEBHOOK_EVENTS.md)** - Xử lý real-time events
|
|
708
|
+
|
|
709
|
+
Tham khảo **[API Reference](./API_REFERENCE.md)** để biết chi tiết về tất cả methods có sẵn.
|