@cabin-id/nextjs 1.2.5 → 1.2.7
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 +579 -0
- package/dist/cjs/components/SignInButton.js +6 -3
- package/dist/cjs/components/SignInButton.js.map +1 -1
- package/dist/cjs/utils/initial.js +1 -1
- package/dist/cjs/utils/initial.js.map +1 -1
- package/dist/esm/components/SignInButton.js +6 -3
- package/dist/esm/components/SignInButton.js.map +1 -1
- package/dist/esm/utils/initial.js +1 -1
- package/dist/esm/utils/initial.js.map +1 -1
- package/dist/types/components/SignInButton.d.ts +15 -1
- package/dist/types/components/SignInButton.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
# CabinID SDK
|
|
2
|
+
|
|
3
|
+
CabinID SDK là một bộ công cụ xác thực và quản lý danh tính đa cấp, cung cấp giải pháp tích hợp dễ dàng cho các ứng dụng Next.js và React.
|
|
4
|
+
|
|
5
|
+
## 📦 Cài đặt
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @cabin-id/nextjs
|
|
9
|
+
# hoặc
|
|
10
|
+
pnpm add @cabin-id/nextjs
|
|
11
|
+
# hoặc
|
|
12
|
+
yarn add @cabin-id/nextjs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 🚀 Cấu hình nhanh
|
|
16
|
+
|
|
17
|
+
### 1. Thiết lập biến môi trường
|
|
18
|
+
|
|
19
|
+
Tạo file `.env.local` trong thư mục gốc dự án của bạn:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# CabinID Configuration
|
|
23
|
+
NEXT_PUBLIC_CABIN_ID_PUBLISH_KEY=cabin_pk_your_publishable_key_here
|
|
24
|
+
CABIN_ID_SECRET_KEY=cabin_sk_your_secret_key_here
|
|
25
|
+
|
|
26
|
+
# URL Redirects
|
|
27
|
+
NEXT_PUBLIC_CABIN_ID_AFTER_SIGN_IN_URL=/dashboard
|
|
28
|
+
NEXT_PUBLIC_CABIN_ID_AFTER_SIGN_UP_URL=/welcome
|
|
29
|
+
NEXT_PUBLIC_CABIN_ID_SIGN_IN_URL=/auth
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. Cấu hình CabinIdProvider
|
|
33
|
+
|
|
34
|
+
Bọc ứng dụng của bạn với `CabinIdProvider`:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// app/layout.tsx hoặc pages/_app.tsx
|
|
38
|
+
'use client';
|
|
39
|
+
|
|
40
|
+
import { CabinIdProvider } from '@cabin-id/nextjs';
|
|
41
|
+
|
|
42
|
+
export default function RootLayout({
|
|
43
|
+
children,
|
|
44
|
+
}: {
|
|
45
|
+
children: React.ReactNode;
|
|
46
|
+
}) {
|
|
47
|
+
return (
|
|
48
|
+
<html lang="vi">
|
|
49
|
+
<body>
|
|
50
|
+
<CabinIdProvider>
|
|
51
|
+
{children}
|
|
52
|
+
</CabinIdProvider>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Middleware (Tùy chọn)
|
|
60
|
+
|
|
61
|
+
Tạo file `middleware.ts` trong thư mục gốc để bảo vệ các route:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// middleware.ts
|
|
65
|
+
import { authMiddleware } from '@cabin-id/nextjs/server';
|
|
66
|
+
|
|
67
|
+
export default authMiddleware({
|
|
68
|
+
publicRoutes: ['/'],
|
|
69
|
+
afterAuth(auth, req) {
|
|
70
|
+
// Xử lý sau khi xác thực
|
|
71
|
+
if (!auth.userId && !auth.isPublicRoute) {
|
|
72
|
+
return Response.redirect(new URL('/auth', req.url));
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const config = {
|
|
78
|
+
matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 🔐 Tích hợp Authentication
|
|
83
|
+
|
|
84
|
+
### SignInButton - Nút Đăng nhập
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
'use client';
|
|
88
|
+
|
|
89
|
+
import { SignInButton } from '@cabin-id/nextjs';
|
|
90
|
+
|
|
91
|
+
export default function LoginPage() {
|
|
92
|
+
return (
|
|
93
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
94
|
+
<div className="max-w-md w-full space-y-8">
|
|
95
|
+
<div>
|
|
96
|
+
<h2 className="text-center text-3xl font-extrabold text-gray-900">
|
|
97
|
+
Đăng nhập vào tài khoản
|
|
98
|
+
</h2>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<SignInButton />
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### SignOutButton - Nút Đăng xuất
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
'use client';
|
|
112
|
+
|
|
113
|
+
import { SignOutButton } from '@cabin-id/nextjs';
|
|
114
|
+
|
|
115
|
+
export default function Dashboard() {
|
|
116
|
+
return (
|
|
117
|
+
<div className="min-h-screen bg-gray-50">
|
|
118
|
+
<nav className="bg-white shadow">
|
|
119
|
+
<div className="max-w-7xl mx-auto px-4">
|
|
120
|
+
<div className="flex justify-between h-16">
|
|
121
|
+
<div className="flex items-center">
|
|
122
|
+
<h1 className="text-xl font-semibold">Dashboard</h1>
|
|
123
|
+
</div>
|
|
124
|
+
<div className="flex items-center">
|
|
125
|
+
<SignOutButton
|
|
126
|
+
className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700"
|
|
127
|
+
onSignOutSuccess={() => {
|
|
128
|
+
console.log('Đăng xuất thành công');
|
|
129
|
+
}}
|
|
130
|
+
onSignOutError={(error) => {
|
|
131
|
+
console.error('Lỗi đăng xuất:', error);
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
Đăng xuất
|
|
135
|
+
</SignOutButton>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</nav>
|
|
140
|
+
|
|
141
|
+
{/* Nội dung dashboard */}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### useUser Hook - Quản lý trạng thái người dùng
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
'use client';
|
|
151
|
+
|
|
152
|
+
import { useUser } from '@cabin-id/nextjs';
|
|
153
|
+
|
|
154
|
+
export default function ProfilePage() {
|
|
155
|
+
const { user, isLoaded, isSignedIn } = useUser();
|
|
156
|
+
|
|
157
|
+
if (!isLoaded) {
|
|
158
|
+
return <div>Đang tải...</div>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!isSignedIn) {
|
|
162
|
+
return <div>Vui lòng đăng nhập để tiếp tục</div>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className="max-w-4xl mx-auto p-6">
|
|
167
|
+
<h1 className="text-2xl font-bold mb-4">Thông tin cá nhân</h1>
|
|
168
|
+
|
|
169
|
+
<div className="bg-white shadow rounded-lg p-6">
|
|
170
|
+
<div className="space-y-4">
|
|
171
|
+
<div>
|
|
172
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
173
|
+
ID người dùng
|
|
174
|
+
</label>
|
|
175
|
+
<p className="mt-1 text-sm text-gray-900">{user.id}</p>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div>
|
|
179
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
180
|
+
Email
|
|
181
|
+
</label>
|
|
182
|
+
<p className="mt-1 text-sm text-gray-900">{user.primaryEmailAddress?.emailAddress}</p>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div>
|
|
186
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
187
|
+
Số điện thoại
|
|
188
|
+
</label>
|
|
189
|
+
<p className="mt-1 text-sm text-gray-900">{user.primaryPhoneNumber?.phoneNumber}</p>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div>
|
|
193
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
194
|
+
Ngày tạo
|
|
195
|
+
</label>
|
|
196
|
+
<p className="mt-1 text-sm text-gray-900">{user.createdAt}</p>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## 🌐 Tích hợp cho Website Bên ngoài
|
|
206
|
+
|
|
207
|
+
### Ví dụ: Tích hợp vào website hiện có
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
// components/AuthButton.tsx
|
|
211
|
+
'use client';
|
|
212
|
+
|
|
213
|
+
import { useUser, SignInButton, SignOutButton } from '@cabin-id/nextjs';
|
|
214
|
+
|
|
215
|
+
export default function AuthButton() {
|
|
216
|
+
const { isSignedIn, user, isLoaded } = useUser();
|
|
217
|
+
|
|
218
|
+
if (!isLoaded) {
|
|
219
|
+
return (
|
|
220
|
+
<div className="animate-pulse">
|
|
221
|
+
<div className="h-10 w-24 bg-gray-200 rounded"></div>
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!isSignedIn) {
|
|
227
|
+
return <SignInButton />;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div className="flex items-center space-x-4">
|
|
232
|
+
<span className="text-sm text-gray-700">
|
|
233
|
+
Xin chào, {user.firstName || 'Người dùng'}
|
|
234
|
+
</span>
|
|
235
|
+
<SignOutButton
|
|
236
|
+
className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700"
|
|
237
|
+
onSignOutSuccess={() => {
|
|
238
|
+
// Có thể thêm logic tùy chỉnh sau khi đăng xuất
|
|
239
|
+
window.location.reload();
|
|
240
|
+
}}
|
|
241
|
+
>
|
|
242
|
+
Đăng xuất
|
|
243
|
+
</SignOutButton>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Bảo vệ các trang riêng tư
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
// components/ProtectedRoute.tsx
|
|
253
|
+
'use client';
|
|
254
|
+
|
|
255
|
+
import { useUser } from '@cabin-id/nextjs';
|
|
256
|
+
import { useRouter } from 'next/navigation';
|
|
257
|
+
import { useEffect } from 'react';
|
|
258
|
+
|
|
259
|
+
interface ProtectedRouteProps {
|
|
260
|
+
children: React.ReactNode;
|
|
261
|
+
fallback?: React.ReactNode;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default function ProtectedRoute({
|
|
265
|
+
children,
|
|
266
|
+
fallback = <div>Đang chuyển hướng...</div>
|
|
267
|
+
}: ProtectedRouteProps) {
|
|
268
|
+
const { isSignedIn, isLoaded } = useUser();
|
|
269
|
+
const router = useRouter();
|
|
270
|
+
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
if (isLoaded && !isSignedIn) {
|
|
273
|
+
router.push('/auth');
|
|
274
|
+
}
|
|
275
|
+
}, [isLoaded, isSignedIn, router]);
|
|
276
|
+
|
|
277
|
+
if (!isLoaded) {
|
|
278
|
+
return <div>Đang tải...</div>;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!isSignedIn) {
|
|
282
|
+
return fallback;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return <>{children}</>;
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Sử dụng:
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
// app/dashboard/page.tsx
|
|
293
|
+
import ProtectedRoute from '@/components/ProtectedRoute';
|
|
294
|
+
|
|
295
|
+
export default function DashboardPage() {
|
|
296
|
+
return (
|
|
297
|
+
<ProtectedRoute>
|
|
298
|
+
<div className="p-6">
|
|
299
|
+
<h1 className="text-2xl font-bold">Dashboard</h1>
|
|
300
|
+
<p>Đây là trang chỉ dành cho người dùng đã đăng nhập.</p>
|
|
301
|
+
</div>
|
|
302
|
+
</ProtectedRoute>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## 🛠️ Server-side Functions
|
|
308
|
+
|
|
309
|
+
### getCurrentUser - Lấy thông tin người dùng từ server
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
// app/api/profile/route.ts
|
|
313
|
+
import { getCurrentUser } from '@cabin-id/nextjs/server';
|
|
314
|
+
import { NextResponse } from 'next/server';
|
|
315
|
+
|
|
316
|
+
export async function GET() {
|
|
317
|
+
try {
|
|
318
|
+
const user = await getCurrentUser();
|
|
319
|
+
|
|
320
|
+
if (!user) {
|
|
321
|
+
return NextResponse.json(
|
|
322
|
+
{ error: 'Chưa đăng nhập' },
|
|
323
|
+
{ status: 401 }
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return NextResponse.json({ user });
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return NextResponse.json(
|
|
330
|
+
{ error: 'Lỗi server' },
|
|
331
|
+
{ status: 500 }
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### protect - Bảo vệ API routes
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
// app/api/admin/route.ts
|
|
341
|
+
import { protect } from '@cabin-id/nextjs/server';
|
|
342
|
+
import { NextResponse } from 'next/server';
|
|
343
|
+
|
|
344
|
+
export const GET = protect(async (req, { user }) => {
|
|
345
|
+
// Chỉ người dùng đã đăng nhập mới có thể truy cập
|
|
346
|
+
return NextResponse.json({
|
|
347
|
+
message: `Xin chào ${user.firstName}!`,
|
|
348
|
+
adminData: 'Dữ liệu quan trọng'
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## 🎨 Tùy chỉnh CSS
|
|
354
|
+
|
|
355
|
+
### Tùy chỉnh SignInButton
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
import { SignInButton } from '@cabin-id/nextjs';
|
|
359
|
+
|
|
360
|
+
export default function CustomSignIn() {
|
|
361
|
+
return (
|
|
362
|
+
<SignInButton
|
|
363
|
+
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
364
|
+
/>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Tùy chỉnh SignOutButton
|
|
370
|
+
|
|
371
|
+
```tsx
|
|
372
|
+
import { SignOutButton } from '@cabin-id/nextjs';
|
|
373
|
+
|
|
374
|
+
export default function CustomSignOut() {
|
|
375
|
+
return (
|
|
376
|
+
<SignOutButton
|
|
377
|
+
className="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
378
|
+
showLoading={true}
|
|
379
|
+
loadingText="Đang đăng xuất..."
|
|
380
|
+
onSignOutStart={() => console.log('Bắt đầu đăng xuất')}
|
|
381
|
+
onSignOutSuccess={() => console.log('Đăng xuất thành công')}
|
|
382
|
+
onSignOutError={(error) => console.error('Lỗi đăng xuất:', error)}
|
|
383
|
+
>
|
|
384
|
+
<svg className="mr-2 -ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
385
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
|
386
|
+
</svg>
|
|
387
|
+
Đăng xuất
|
|
388
|
+
</SignOutButton>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## 📝 Ví dụ hoàn chỉnh - Website Thương mại điện tử
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
// app/layout.tsx
|
|
397
|
+
'use client';
|
|
398
|
+
|
|
399
|
+
import { CabinIdProvider } from '@cabin-id/nextjs';
|
|
400
|
+
import './globals.css';
|
|
401
|
+
|
|
402
|
+
export default function RootLayout({
|
|
403
|
+
children,
|
|
404
|
+
}: {
|
|
405
|
+
children: React.ReactNode;
|
|
406
|
+
}) {
|
|
407
|
+
return (
|
|
408
|
+
<html lang="vi">
|
|
409
|
+
<body>
|
|
410
|
+
<CabinIdProvider>
|
|
411
|
+
<Header />
|
|
412
|
+
<main>{children}</main>
|
|
413
|
+
<Footer />
|
|
414
|
+
</CabinIdProvider>
|
|
415
|
+
</body>
|
|
416
|
+
</html>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// components/Header.tsx
|
|
421
|
+
'use client';
|
|
422
|
+
|
|
423
|
+
import { useUser, SignInButton, SignOutButton } from '@cabin-id/nextjs';
|
|
424
|
+
import Link from 'next/link';
|
|
425
|
+
|
|
426
|
+
function Header() {
|
|
427
|
+
const { isSignedIn, user, isLoaded } = useUser();
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
<header className="bg-white shadow">
|
|
431
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
432
|
+
<div className="flex justify-between h-16">
|
|
433
|
+
<div className="flex items-center">
|
|
434
|
+
<Link href="/" className="text-xl font-bold text-gray-900">
|
|
435
|
+
Cửa hàng của tôi
|
|
436
|
+
</Link>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<nav className="hidden md:flex items-center space-x-8">
|
|
440
|
+
<Link href="/products" className="text-gray-500 hover:text-gray-900">
|
|
441
|
+
Sản phẩm
|
|
442
|
+
</Link>
|
|
443
|
+
<Link href="/about" className="text-gray-500 hover:text-gray-900">
|
|
444
|
+
Về chúng tôi
|
|
445
|
+
</Link>
|
|
446
|
+
<Link href="/contact" className="text-gray-500 hover:text-gray-900">
|
|
447
|
+
Liên hệ
|
|
448
|
+
</Link>
|
|
449
|
+
</nav>
|
|
450
|
+
|
|
451
|
+
<div className="flex items-center space-x-4">
|
|
452
|
+
{!isLoaded ? (
|
|
453
|
+
<div className="animate-pulse h-10 w-24 bg-gray-200 rounded"></div>
|
|
454
|
+
) : isSignedIn ? (
|
|
455
|
+
<div className="flex items-center space-x-4">
|
|
456
|
+
<Link
|
|
457
|
+
href="/profile"
|
|
458
|
+
className="text-gray-700 hover:text-gray-900"
|
|
459
|
+
>
|
|
460
|
+
Xin chào, {user.firstName}
|
|
461
|
+
</Link>
|
|
462
|
+
<SignOutButton
|
|
463
|
+
className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700"
|
|
464
|
+
>
|
|
465
|
+
Đăng xuất
|
|
466
|
+
</SignOutButton>
|
|
467
|
+
</div>
|
|
468
|
+
) : (
|
|
469
|
+
<SignInButton />
|
|
470
|
+
)}
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
</header>
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## 🔧 Cấu hình nâng cao
|
|
480
|
+
|
|
481
|
+
### Tùy chỉnh Domain
|
|
482
|
+
|
|
483
|
+
```tsx
|
|
484
|
+
// middleware.ts
|
|
485
|
+
import { authMiddleware } from '@cabin-id/nextjs/server';
|
|
486
|
+
|
|
487
|
+
export default authMiddleware({
|
|
488
|
+
// Domain tùy chỉnh
|
|
489
|
+
domain: 'myapp.cabinid.dev',
|
|
490
|
+
|
|
491
|
+
// Các route không cần xác thực
|
|
492
|
+
publicRoutes: ['/', '/products', '/about', '/contact'],
|
|
493
|
+
|
|
494
|
+
// Các route chỉ dành cho admin
|
|
495
|
+
adminRoutes: ['/admin'],
|
|
496
|
+
|
|
497
|
+
// Xử lý sau khi xác thực
|
|
498
|
+
afterAuth(auth, req) {
|
|
499
|
+
const { userId, isPublicRoute, isAdminRoute } = auth;
|
|
500
|
+
|
|
501
|
+
// Chuyển hướng người dùng chưa đăng nhập
|
|
502
|
+
if (!userId && !isPublicRoute) {
|
|
503
|
+
return Response.redirect(new URL('/auth', req.url));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Kiểm tra quyền admin (cần implement logic kiểm tra)
|
|
507
|
+
if (isAdminRoute && !isAdmin(userId)) {
|
|
508
|
+
return Response.redirect(new URL('/unauthorized', req.url));
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
function isAdmin(userId: string): boolean {
|
|
514
|
+
// Logic kiểm tra quyền admin
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
## 📚 API Reference
|
|
520
|
+
|
|
521
|
+
### Components
|
|
522
|
+
|
|
523
|
+
- `SignInButton`: Nút đăng nhập tự động chuyển hướng đến CabinID
|
|
524
|
+
- `SignOutButton`: Nút đăng xuất với xử lý callback
|
|
525
|
+
- `CabinIdProvider`: Provider chính để quản lý trạng thái xác thực
|
|
526
|
+
|
|
527
|
+
### Hooks
|
|
528
|
+
|
|
529
|
+
- `useUser()`: Hook để lấy thông tin người dùng hiện tại
|
|
530
|
+
|
|
531
|
+
### Server Functions
|
|
532
|
+
|
|
533
|
+
- `getCurrentUser()`: Lấy thông tin người dùng từ server
|
|
534
|
+
- `protect()`: Bảo vệ API routes
|
|
535
|
+
- `authMiddleware()`: Middleware xác thực cho Next.js
|
|
536
|
+
|
|
537
|
+
## 🐛 Xử lý lỗi thường gặp
|
|
538
|
+
|
|
539
|
+
### Lỗi: "CabinIdProvider not found"
|
|
540
|
+
|
|
541
|
+
Đảm bảo bạn đã bọc ứng dụng với `CabinIdProvider`:
|
|
542
|
+
|
|
543
|
+
```tsx
|
|
544
|
+
// ✅ Đúng
|
|
545
|
+
<CabinIdProvider>
|
|
546
|
+
<YourApp />
|
|
547
|
+
</CabinIdProvider>
|
|
548
|
+
|
|
549
|
+
// ❌ Sai - thiếu provider
|
|
550
|
+
<YourApp />
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Lỗi: Biến môi trường không được tìm thấy
|
|
554
|
+
|
|
555
|
+
Kiểm tra file `.env.local` và đảm bảo tên biến chính xác:
|
|
556
|
+
|
|
557
|
+
```bash
|
|
558
|
+
# ✅ Đúng
|
|
559
|
+
NEXT_PUBLIC_CABIN_ID_PUBLISH_KEY=cabin_pk_...
|
|
560
|
+
CABIN_ID_SECRET_KEY=cabin_sk_...
|
|
561
|
+
|
|
562
|
+
# ❌ Sai - thiếu NEXT_PUBLIC_
|
|
563
|
+
CABIN_ID_PUBLISH_KEY=cabin_pk_...
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
## 🔗 Liên kết hữu ích
|
|
567
|
+
|
|
568
|
+
- [Trang chủ CabinID](https://cabinid.dev)
|
|
569
|
+
- [Dashboard CabinID](https://dashboard.cabinid.dev)
|
|
570
|
+
- [Tài liệu API](https://docs.cabinid.dev)
|
|
571
|
+
- [Báo lỗi](https://github.com/cabinvn/issues)
|
|
572
|
+
|
|
573
|
+
## 📄 Giấy phép
|
|
574
|
+
|
|
575
|
+
MIT License - xem file [LICENSE](LICENSE) để biết thêm chi tiết.
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
🚀 **Bắt đầu ngay hôm nay!** Tích hợp CabinID vào ứng dụng của bạn chỉ trong vài phút.
|
|
@@ -35,7 +35,10 @@ module.exports = __toCommonJS(SignInButton_exports);
|
|
|
35
35
|
var import_react = __toESM(require("react"));
|
|
36
36
|
var import_logo = require("../icons/logo");
|
|
37
37
|
var import_constants = require("../constants");
|
|
38
|
-
const SignInButton = (
|
|
38
|
+
const SignInButton = ({
|
|
39
|
+
children = "Continue to CabinID",
|
|
40
|
+
className = "max-w-48 pl-2 py-2 pr-4 bg-blue-600 rounded-full flex flex-row space-x-2 items-center hover:opacity-90 active:bg-blue-700"
|
|
41
|
+
}) => {
|
|
39
42
|
const [signInUrl, setSignInUrl] = (0, import_react.useState)("#");
|
|
40
43
|
(0, import_react.useEffect)(() => {
|
|
41
44
|
if (typeof window !== "undefined") {
|
|
@@ -54,10 +57,10 @@ const SignInButton = () => {
|
|
|
54
57
|
"a",
|
|
55
58
|
{
|
|
56
59
|
href: signInUrl,
|
|
57
|
-
className
|
|
60
|
+
className
|
|
58
61
|
},
|
|
59
62
|
/* @__PURE__ */ import_react.default.createElement(import_logo.CabinLogo, null),
|
|
60
|
-
/* @__PURE__ */ import_react.default.createElement("span", { className: "text-white text-sm" },
|
|
63
|
+
/* @__PURE__ */ import_react.default.createElement("span", { className: "text-white text-sm" }, children)
|
|
61
64
|
);
|
|
62
65
|
};
|
|
63
66
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/SignInButton.tsx"],"sourcesContent":["'use client';\n\nimport React, { useEffect, useState } from 'react';\nimport { CabinLogo } from '../icons/logo';\nimport { AFTER_SIGN_IN_URL, frontendApi } from '../constants';\n\nexport const SignInButton = () => {\n const [signInUrl, setSignInUrl] = useState('#');\n\n useEffect(() => {\n if (typeof window !== 'undefined') {\n const origin = typeof window === 'undefined' ? null : window.origin;\n if (!origin) {\n return;\n }\n const params = new URLSearchParams({\n redirect_url: `${origin}${AFTER_SIGN_IN_URL}` || '/',\n });\n const signInUrlTemp = frontendApi\n ? `https://${frontendApi}/sign-in?${params.toString()}`\n : '#';\n setSignInUrl(signInUrlTemp);\n }\n }, []);\n\n return (\n <a\n href={signInUrl}\n className
|
|
1
|
+
{"version":3,"sources":["../../../src/components/SignInButton.tsx"],"sourcesContent":["'use client';\n\nimport React, { useEffect, useState } from 'react';\nimport { CabinLogo } from '../icons/logo';\nimport { AFTER_SIGN_IN_URL, frontendApi } from '../constants';\n\nexport interface SignInButtonProps {\n /**\n * Custom text to display on the button\n * @default \"Continue to CabinID\"\n */\n children?: React.ReactNode;\n /**\n * Custom CSS class name\n */\n className?: string;\n}\n\n/**\n * SignInButton component that handles user authentication redirect to CabinID\n */\nexport const SignInButton: React.FC<SignInButtonProps> = ({\n children = 'Continue to CabinID',\n className = 'max-w-48 pl-2 py-2 pr-4 bg-blue-600 rounded-full flex flex-row space-x-2 items-center hover:opacity-90 active:bg-blue-700',\n}) => {\n const [signInUrl, setSignInUrl] = useState('#');\n\n useEffect(() => {\n if (typeof window !== 'undefined') {\n const origin = typeof window === 'undefined' ? null : window.origin;\n if (!origin) {\n return;\n }\n const params = new URLSearchParams({\n redirect_url: `${origin}${AFTER_SIGN_IN_URL}` || '/',\n });\n const signInUrlTemp = frontendApi\n ? `https://${frontendApi}/sign-in?${params.toString()}`\n : '#';\n setSignInUrl(signInUrlTemp);\n }\n }, []);\n\n return (\n <a\n href={signInUrl}\n className={className}\n >\n <CabinLogo />\n <span className=\"text-white text-sm\">{children}</span>\n </a>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,mBAA2C;AAC3C,kBAA0B;AAC1B,uBAA+C;AAiBxC,MAAM,eAA4C,CAAC;AAAA,EACxD,WAAW;AAAA,EACX,YAAY;AACd,MAAM;AACJ,QAAM,CAAC,WAAW,YAAY,QAAI,uBAAS,GAAG;AAE9C,8BAAU,MAAM;AACd,QAAI,OAAO,WAAW,aAAa;AACjC,YAAM,SAAS,OAAO,WAAW,cAAc,OAAO,OAAO;AAC7D,UAAI,CAAC,QAAQ;AACX;AAAA,MACF;AACA,YAAM,SAAS,IAAI,gBAAgB;AAAA,QACjC,cAAc,GAAG,MAAM,GAAG,kCAAiB,MAAM;AAAA,MACnD,CAAC;AACD,YAAM,gBAAgB,+BAClB,WAAW,4BAAW,YAAY,OAAO,SAAS,CAAC,KACnD;AACJ,mBAAa,aAAa;AAAA,IAC5B;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SACE,6BAAAA,QAAA;AAAA,IAAC;AAAA;AAAA,MACC,MAAM;AAAA,MACN;AAAA;AAAA,IAEA,6BAAAA,QAAA,cAAC,2BAAU;AAAA,IACX,6BAAAA,QAAA,cAAC,UAAK,WAAU,wBAAsB,QAAS;AAAA,EACjD;AAEJ;","names":["React"]}
|
|
@@ -27,7 +27,7 @@ var import_constants = require("../constants");
|
|
|
27
27
|
var import_http = require("./http");
|
|
28
28
|
const initialState = async () => {
|
|
29
29
|
var _a;
|
|
30
|
-
const cookiesStore = (0, import_headers.cookies)();
|
|
30
|
+
const cookiesStore = await (0, import_headers.cookies)();
|
|
31
31
|
const userId = ((_a = cookiesStore.get(import_constants.constants.Cookies.User)) == null ? void 0 : _a.value) || null;
|
|
32
32
|
let user = null;
|
|
33
33
|
if (userId) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/utils/initial.ts"],"sourcesContent":["'use server';\n\nimport { cookies } from 'next/headers';\nimport { constants } from '../constants';\nimport { client } from './http';\n\nexport const initialState = async () => {\n const cookiesStore = cookies();\n const userId = cookiesStore.get(constants.Cookies.User)?.value || null;\n let user = null;\n\n if (userId) {\n user = await client.users.getUser(userId);\n }\n\n return {\n userId,\n user,\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,qBAAwB;AACxB,uBAA0B;AAC1B,kBAAuB;AAEhB,MAAM,eAAe,YAAY;AANxC;AAOE,QAAM,
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/initial.ts"],"sourcesContent":["'use server';\n\nimport { cookies } from 'next/headers';\nimport { constants } from '../constants';\nimport { client } from './http';\n\nexport const initialState = async () => {\n const cookiesStore = await cookies();\n const userId = cookiesStore.get(constants.Cookies.User)?.value || null;\n let user = null;\n\n if (userId) {\n user = await client.users.getUser(userId);\n }\n\n return {\n userId,\n user,\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,qBAAwB;AACxB,uBAA0B;AAC1B,kBAAuB;AAEhB,MAAM,eAAe,YAAY;AANxC;AAOE,QAAM,eAAe,UAAM,wBAAQ;AACnC,QAAM,WAAS,kBAAa,IAAI,2BAAU,QAAQ,IAAI,MAAvC,mBAA0C,UAAS;AAClE,MAAI,OAAO;AAEX,MAAI,QAAQ;AACV,WAAO,MAAM,mBAAO,MAAM,QAAQ,MAAM;AAAA,EAC1C;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
import React, { useEffect, useState } from "react";
|
|
3
3
|
import { CabinLogo } from "../icons/logo";
|
|
4
4
|
import { AFTER_SIGN_IN_URL, frontendApi } from "../constants";
|
|
5
|
-
const SignInButton = (
|
|
5
|
+
const SignInButton = ({
|
|
6
|
+
children = "Continue to CabinID",
|
|
7
|
+
className = "max-w-48 pl-2 py-2 pr-4 bg-blue-600 rounded-full flex flex-row space-x-2 items-center hover:opacity-90 active:bg-blue-700"
|
|
8
|
+
}) => {
|
|
6
9
|
const [signInUrl, setSignInUrl] = useState("#");
|
|
7
10
|
useEffect(() => {
|
|
8
11
|
if (typeof window !== "undefined") {
|
|
@@ -21,10 +24,10 @@ const SignInButton = () => {
|
|
|
21
24
|
"a",
|
|
22
25
|
{
|
|
23
26
|
href: signInUrl,
|
|
24
|
-
className
|
|
27
|
+
className
|
|
25
28
|
},
|
|
26
29
|
/* @__PURE__ */ React.createElement(CabinLogo, null),
|
|
27
|
-
/* @__PURE__ */ React.createElement("span", { className: "text-white text-sm" },
|
|
30
|
+
/* @__PURE__ */ React.createElement("span", { className: "text-white text-sm" }, children)
|
|
28
31
|
);
|
|
29
32
|
};
|
|
30
33
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/SignInButton.tsx"],"sourcesContent":["'use client';\n\nimport React, { useEffect, useState } from 'react';\nimport { CabinLogo } from '../icons/logo';\nimport { AFTER_SIGN_IN_URL, frontendApi } from '../constants';\n\nexport const SignInButton = () => {\n const [signInUrl, setSignInUrl] = useState('#');\n\n useEffect(() => {\n if (typeof window !== 'undefined') {\n const origin = typeof window === 'undefined' ? null : window.origin;\n if (!origin) {\n return;\n }\n const params = new URLSearchParams({\n redirect_url: `${origin}${AFTER_SIGN_IN_URL}` || '/',\n });\n const signInUrlTemp = frontendApi\n ? `https://${frontendApi}/sign-in?${params.toString()}`\n : '#';\n setSignInUrl(signInUrlTemp);\n }\n }, []);\n\n return (\n <a\n href={signInUrl}\n className
|
|
1
|
+
{"version":3,"sources":["../../../src/components/SignInButton.tsx"],"sourcesContent":["'use client';\n\nimport React, { useEffect, useState } from 'react';\nimport { CabinLogo } from '../icons/logo';\nimport { AFTER_SIGN_IN_URL, frontendApi } from '../constants';\n\nexport interface SignInButtonProps {\n /**\n * Custom text to display on the button\n * @default \"Continue to CabinID\"\n */\n children?: React.ReactNode;\n /**\n * Custom CSS class name\n */\n className?: string;\n}\n\n/**\n * SignInButton component that handles user authentication redirect to CabinID\n */\nexport const SignInButton: React.FC<SignInButtonProps> = ({\n children = 'Continue to CabinID',\n className = 'max-w-48 pl-2 py-2 pr-4 bg-blue-600 rounded-full flex flex-row space-x-2 items-center hover:opacity-90 active:bg-blue-700',\n}) => {\n const [signInUrl, setSignInUrl] = useState('#');\n\n useEffect(() => {\n if (typeof window !== 'undefined') {\n const origin = typeof window === 'undefined' ? null : window.origin;\n if (!origin) {\n return;\n }\n const params = new URLSearchParams({\n redirect_url: `${origin}${AFTER_SIGN_IN_URL}` || '/',\n });\n const signInUrlTemp = frontendApi\n ? `https://${frontendApi}/sign-in?${params.toString()}`\n : '#';\n setSignInUrl(signInUrlTemp);\n }\n }, []);\n\n return (\n <a\n href={signInUrl}\n className={className}\n >\n <CabinLogo />\n <span className=\"text-white text-sm\">{children}</span>\n </a>\n );\n};\n"],"mappings":";AAEA,OAAO,SAAS,WAAW,gBAAgB;AAC3C,SAAS,iBAAiB;AAC1B,SAAS,mBAAmB,mBAAmB;AAiBxC,MAAM,eAA4C,CAAC;AAAA,EACxD,WAAW;AAAA,EACX,YAAY;AACd,MAAM;AACJ,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,GAAG;AAE9C,YAAU,MAAM;AACd,QAAI,OAAO,WAAW,aAAa;AACjC,YAAM,SAAS,OAAO,WAAW,cAAc,OAAO,OAAO;AAC7D,UAAI,CAAC,QAAQ;AACX;AAAA,MACF;AACA,YAAM,SAAS,IAAI,gBAAgB;AAAA,QACjC,cAAc,GAAG,MAAM,GAAG,iBAAiB,MAAM;AAAA,MACnD,CAAC;AACD,YAAM,gBAAgB,cAClB,WAAW,WAAW,YAAY,OAAO,SAAS,CAAC,KACnD;AACJ,mBAAa,aAAa;AAAA,IAC5B;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM;AAAA,MACN;AAAA;AAAA,IAEA,oCAAC,eAAU;AAAA,IACX,oCAAC,UAAK,WAAU,wBAAsB,QAAS;AAAA,EACjD;AAEJ;","names":[]}
|
|
@@ -4,7 +4,7 @@ import { constants } from "../constants";
|
|
|
4
4
|
import { client } from "./http";
|
|
5
5
|
const initialState = async () => {
|
|
6
6
|
var _a;
|
|
7
|
-
const cookiesStore = cookies();
|
|
7
|
+
const cookiesStore = await cookies();
|
|
8
8
|
const userId = ((_a = cookiesStore.get(constants.Cookies.User)) == null ? void 0 : _a.value) || null;
|
|
9
9
|
let user = null;
|
|
10
10
|
if (userId) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/utils/initial.ts"],"sourcesContent":["'use server';\n\nimport { cookies } from 'next/headers';\nimport { constants } from '../constants';\nimport { client } from './http';\n\nexport const initialState = async () => {\n const cookiesStore = cookies();\n const userId = cookiesStore.get(constants.Cookies.User)?.value || null;\n let user = null;\n\n if (userId) {\n user = await client.users.getUser(userId);\n }\n\n return {\n userId,\n user,\n };\n};\n"],"mappings":";AAEA,SAAS,eAAe;AACxB,SAAS,iBAAiB;AAC1B,SAAS,cAAc;AAEhB,MAAM,eAAe,YAAY;AANxC;AAOE,QAAM,eAAe,QAAQ;
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/initial.ts"],"sourcesContent":["'use server';\n\nimport { cookies } from 'next/headers';\nimport { constants } from '../constants';\nimport { client } from './http';\n\nexport const initialState = async () => {\n const cookiesStore = await cookies();\n const userId = cookiesStore.get(constants.Cookies.User)?.value || null;\n let user = null;\n\n if (userId) {\n user = await client.users.getUser(userId);\n }\n\n return {\n userId,\n user,\n };\n};\n"],"mappings":";AAEA,SAAS,eAAe;AACxB,SAAS,iBAAiB;AAC1B,SAAS,cAAc;AAEhB,MAAM,eAAe,YAAY;AANxC;AAOE,QAAM,eAAe,MAAM,QAAQ;AACnC,QAAM,WAAS,kBAAa,IAAI,UAAU,QAAQ,IAAI,MAAvC,mBAA0C,UAAS;AAClE,MAAI,OAAO;AAEX,MAAI,QAAQ;AACV,WAAO,MAAM,OAAO,MAAM,QAAQ,MAAM;AAAA,EAC1C;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
export
|
|
2
|
+
export interface SignInButtonProps {
|
|
3
|
+
/**
|
|
4
|
+
* Custom text to display on the button
|
|
5
|
+
* @default "Continue to CabinID"
|
|
6
|
+
*/
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* Custom CSS class name
|
|
10
|
+
*/
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* SignInButton component that handles user authentication redirect to CabinID
|
|
15
|
+
*/
|
|
16
|
+
export declare const SignInButton: React.FC<SignInButtonProps>;
|
|
3
17
|
//# sourceMappingURL=SignInButton.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SignInButton.d.ts","sourceRoot":"","sources":["../../../src/components/SignInButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD,eAAO,MAAM,YAAY,
|
|
1
|
+
{"version":3,"file":"SignInButton.d.ts","sourceRoot":"","sources":["../../../src/components/SignInButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA+BpD,CAAC"}
|