@udhuong/admin-core 0.1.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/LICENSE +21 -0
- package/README.md +407 -0
- package/package.json +161 -0
- package/src/client/styles/globals.css +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 udhuong
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# @udhuong/admin-core
|
|
2
|
+
|
|
3
|
+
> Reusable admin core cho Node.js app — auth, RBAC, OAuth, user management. Ship trong **1 package duy nhất** với subpath exports: frontend (Next.js) và backend (NestJS) cùng trong 1 `npm install`.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@udhuong/admin-core)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
## Tính năng
|
|
9
|
+
|
|
10
|
+
- 🔐 **Auth** — email/password + JWT (access + refresh) + 2FA/TOTP
|
|
11
|
+
- 👥 **User management** — CRUD user, session, preferences (theme/language/notifications)
|
|
12
|
+
- 🛡️ **RBAC dynamic** — core permissions mặc định + consumer extend `extraPermissions`/`extraRoles` qua `forRoot()`
|
|
13
|
+
- 🔗 **OAuth 2.0** — client credentials, authorization code, refresh token; social login Google/GitHub/Facebook
|
|
14
|
+
- 📧 **Mail events** — verification, password reset, welcome (consumer tự cài mail sender)
|
|
15
|
+
- 🎨 **UI ready-to-use** — `<LoginPage />`, `<AdminLayout />`, `<UsersPage />`, `<RolesPage />`, `<PermissionsPage />`… với Tailwind v4 + DaisyUI
|
|
16
|
+
- 🧰 **NestJS module** — `AdminCoreModule.forRoot()` / `forRootAsync()` (global), auto-seed nếu import `OAuthSeederModule`
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @udhuong/admin-core
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Peer dependencies (optional theo phía bạn dùng)
|
|
27
|
+
|
|
28
|
+
**Client (Next.js):**
|
|
29
|
+
```bash
|
|
30
|
+
npm install next react react-dom
|
|
31
|
+
npm install --save-dev tailwindcss @tailwindcss/postcss daisyui
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Server (NestJS):**
|
|
35
|
+
```bash
|
|
36
|
+
npm install @nestjs/common @nestjs/core @nestjs/config @nestjs/event-emitter \
|
|
37
|
+
@nestjs/jwt @nestjs/passport @nestjs/schedule @nestjs/typeorm \
|
|
38
|
+
typeorm pg reflect-metadata rxjs
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Tất cả `@nestjs/*` được khai báo là `peerDependenciesMeta.optional`, nên nếu bạn chỉ dùng client thì **không bị warn** về peer deps server (và ngược lại).
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Quick start — Client (Next.js 16 App Router)
|
|
46
|
+
|
|
47
|
+
### 1. Wrap providers
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// src/app/providers.tsx
|
|
51
|
+
'use client'
|
|
52
|
+
|
|
53
|
+
import { CoreProviders } from '@udhuong/admin-core'
|
|
54
|
+
|
|
55
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
56
|
+
return (
|
|
57
|
+
<CoreProviders
|
|
58
|
+
config={{
|
|
59
|
+
apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api',
|
|
60
|
+
tokenStorageKey: 'admin_token',
|
|
61
|
+
routes: {
|
|
62
|
+
LOGIN: '/login',
|
|
63
|
+
DASHBOARD: '/dashboard',
|
|
64
|
+
UNAUTHORIZED: '/unauthorized',
|
|
65
|
+
CHANGE_PASSWORD: '/change-password',
|
|
66
|
+
},
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</CoreProviders>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`CoreProviders` đã bao gồm `QueryClientProvider`, `ThemeProvider`, `AuthProvider`, `AdminConfigProvider`, `Toaster`. **Không** wrap lại các provider này bên ngoài.
|
|
76
|
+
|
|
77
|
+
### 2. Setup styles (Tailwind v4 + DaisyUI)
|
|
78
|
+
|
|
79
|
+
Tạo `src/app/globals.css`:
|
|
80
|
+
|
|
81
|
+
```css
|
|
82
|
+
@import "tailwindcss";
|
|
83
|
+
@source "../../src/**/*.{ts,tsx}";
|
|
84
|
+
@source "../../node_modules/@udhuong/admin-core/dist/client/**/*.{js,d.ts}";
|
|
85
|
+
@plugin "daisyui" {
|
|
86
|
+
themes: light --default, dark --prefersdark;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`postcss.config.js`:
|
|
91
|
+
```js
|
|
92
|
+
module.exports = {
|
|
93
|
+
plugins: { '@tailwindcss/postcss': {} },
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`next.config.ts`:
|
|
98
|
+
```ts
|
|
99
|
+
export default {
|
|
100
|
+
transpilePackages: ['@udhuong/admin-core'],
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Import vào layout:
|
|
105
|
+
```tsx
|
|
106
|
+
// src/app/layout.tsx
|
|
107
|
+
import './globals.css'
|
|
108
|
+
import { Providers } from './providers'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 3. Dùng các trang có sẵn
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
// src/app/login/page.tsx
|
|
115
|
+
import { LoginPage } from '@udhuong/admin-core'
|
|
116
|
+
export default LoginPage
|
|
117
|
+
|
|
118
|
+
// src/app/(admin)/users/page.tsx
|
|
119
|
+
import { UsersPage } from '@udhuong/admin-core'
|
|
120
|
+
export default UsersPage
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Các trang khác: `RolesPage`, `PermissionsPage`, `ProfilePage`, `UserPreferencesPage`, `ChangePasswordPage`, `ForgotPasswordPage`, `ResetPasswordPage`, `VerifyEmailPage`, `UnauthorizedPage`, `UserDetailPage`.
|
|
124
|
+
|
|
125
|
+
### 4. Dùng hooks
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
'use client'
|
|
129
|
+
import { useAuth } from '@udhuong/admin-core'
|
|
130
|
+
|
|
131
|
+
export default function Dashboard() {
|
|
132
|
+
const { user, logout, isAuthenticated } = useAuth()
|
|
133
|
+
if (!isAuthenticated) return <a href="/login">Login</a>
|
|
134
|
+
return <div>Hi {user?.displayName} <button onClick={logout}>Logout</button></div>
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Quick start — Server (NestJS 10)
|
|
141
|
+
|
|
142
|
+
### 1. Chuẩn bị env vars (BẮT BUỘC)
|
|
143
|
+
|
|
144
|
+
Trước khi boot, cấu hình `.env`:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Root account — password >= 12 ký tự
|
|
148
|
+
ROOT_EMAIL=admin@example.com
|
|
149
|
+
ROOT_PASSWORD=your-strong-root-password
|
|
150
|
+
|
|
151
|
+
# OAuth client secrets — mỗi secret >= 16 ký tự
|
|
152
|
+
# Sinh: openssl rand -base64 32
|
|
153
|
+
OAUTH_WEB_ADMIN_SECRET=<random-32-bytes-base64>
|
|
154
|
+
OAUTH_WEB_PUBLIC_SECRET=<random-32-bytes-base64>
|
|
155
|
+
OAUTH_THIRD_PARTY_DEMO_SECRET=<random-32-bytes-base64>
|
|
156
|
+
OAUTH_API_SERVICE_SECRET=<random-32-bytes-base64>
|
|
157
|
+
|
|
158
|
+
JWT_ACCESS_SECRET=<random-long-string>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 2. Wire AdminCoreModule + seeder
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// src/app.module.ts
|
|
165
|
+
import 'reflect-metadata'
|
|
166
|
+
import { Module } from '@nestjs/common'
|
|
167
|
+
import { ConfigModule, ConfigService } from '@nestjs/config'
|
|
168
|
+
import { EventEmitterModule } from '@nestjs/event-emitter'
|
|
169
|
+
import { TypeOrmModule } from '@nestjs/typeorm'
|
|
170
|
+
import {
|
|
171
|
+
AdminCoreModule,
|
|
172
|
+
OAuthSeederModule,
|
|
173
|
+
PermissionDef,
|
|
174
|
+
RoleDef,
|
|
175
|
+
} from '@udhuong/admin-core/server'
|
|
176
|
+
import { join } from 'path'
|
|
177
|
+
|
|
178
|
+
// Extra permissions + roles riêng của app bạn
|
|
179
|
+
const EXTRA_PERMISSIONS: PermissionDef[] = [
|
|
180
|
+
{ name: 'blog.publish', resource: 'blog', action: 'publish', description: 'Publish blog posts' },
|
|
181
|
+
]
|
|
182
|
+
const EXTRA_ROLES: RoleDef[] = [
|
|
183
|
+
{ name: 'editor', description: 'Blog editor', permissions: ['blog.publish', 'user.read'] },
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
@Module({
|
|
187
|
+
imports: [
|
|
188
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
189
|
+
EventEmitterModule.forRoot(),
|
|
190
|
+
|
|
191
|
+
TypeOrmModule.forRoot({
|
|
192
|
+
type: 'postgres',
|
|
193
|
+
host: 'localhost',
|
|
194
|
+
port: 5432,
|
|
195
|
+
username: 'postgres',
|
|
196
|
+
password: 'postgres',
|
|
197
|
+
database: 'myapp',
|
|
198
|
+
autoLoadEntities: true,
|
|
199
|
+
synchronize: true, // Chỉ dev, production dùng migrations
|
|
200
|
+
// Entities từ admin-core
|
|
201
|
+
entities: [
|
|
202
|
+
join(__dirname, '../node_modules/@udhuong/admin-core/dist/server/**/*.entity.js'),
|
|
203
|
+
],
|
|
204
|
+
}),
|
|
205
|
+
|
|
206
|
+
AdminCoreModule.forRootAsync({
|
|
207
|
+
imports: [ConfigModule],
|
|
208
|
+
inject: [ConfigService],
|
|
209
|
+
useFactory: (config: ConfigService) => ({
|
|
210
|
+
jwt: {
|
|
211
|
+
accessSecret: config.get('JWT_ACCESS_SECRET')!,
|
|
212
|
+
accessExpiresIn: '1h',
|
|
213
|
+
},
|
|
214
|
+
rootEmail: config.get('ROOT_EMAIL')!,
|
|
215
|
+
rootPassword: config.get('ROOT_PASSWORD')!,
|
|
216
|
+
// BẮT BUỘC — seeder sẽ throw nếu thiếu hoặc <16 ký tự
|
|
217
|
+
seeder: {
|
|
218
|
+
clientSecrets: {
|
|
219
|
+
webAdmin: config.get('OAUTH_WEB_ADMIN_SECRET')!,
|
|
220
|
+
webPublic: config.get('OAUTH_WEB_PUBLIC_SECRET')!,
|
|
221
|
+
thirdPartyDemo: config.get('OAUTH_THIRD_PARTY_DEMO_SECRET')!,
|
|
222
|
+
apiService: config.get('OAUTH_API_SERVICE_SECRET')!,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
oauth: {
|
|
226
|
+
google: {
|
|
227
|
+
clientId: config.get('GOOGLE_CLIENT_ID')!,
|
|
228
|
+
clientSecret: config.get('GOOGLE_CLIENT_SECRET')!,
|
|
229
|
+
callbackURL: 'http://localhost:4000/api/auth/google/callback',
|
|
230
|
+
},
|
|
231
|
+
// github, facebook tương tự
|
|
232
|
+
},
|
|
233
|
+
extraPermissions: EXTRA_PERMISSIONS,
|
|
234
|
+
extraRoles: EXTRA_ROLES,
|
|
235
|
+
}),
|
|
236
|
+
}),
|
|
237
|
+
|
|
238
|
+
// Seed permissions + roles + OAuth scopes + clients khi boot
|
|
239
|
+
OAuthSeederModule,
|
|
240
|
+
],
|
|
241
|
+
})
|
|
242
|
+
export class AppModule {}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 2. Enable CORS + bootstrap
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
// src/main.ts
|
|
249
|
+
import 'reflect-metadata'
|
|
250
|
+
import { NestFactory } from '@nestjs/core'
|
|
251
|
+
import { AppModule } from './app.module'
|
|
252
|
+
|
|
253
|
+
async function bootstrap() {
|
|
254
|
+
const app = await NestFactory.create(AppModule)
|
|
255
|
+
app.enableCors({
|
|
256
|
+
origin: process.env.CLIENT_URL || 'http://localhost:3000',
|
|
257
|
+
credentials: true,
|
|
258
|
+
})
|
|
259
|
+
app.setGlobalPrefix('api')
|
|
260
|
+
await app.listen(4000)
|
|
261
|
+
}
|
|
262
|
+
bootstrap()
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 3. tsconfig.json
|
|
266
|
+
|
|
267
|
+
```jsonc
|
|
268
|
+
{
|
|
269
|
+
"compilerOptions": {
|
|
270
|
+
"module": "node16",
|
|
271
|
+
"moduleResolution": "node16",
|
|
272
|
+
"experimentalDecorators": true,
|
|
273
|
+
"emitDecoratorMetadata": true,
|
|
274
|
+
"target": "ES2023"
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 4. Protect endpoint với permission
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import { Controller, Get, UseGuards } from '@nestjs/common'
|
|
283
|
+
import {
|
|
284
|
+
JwtAuthGuard,
|
|
285
|
+
PermissionGuard,
|
|
286
|
+
RequirePermissions,
|
|
287
|
+
CORE_PERMISSIONS,
|
|
288
|
+
} from '@udhuong/admin-core/server'
|
|
289
|
+
|
|
290
|
+
@Controller('admin/blog')
|
|
291
|
+
@UseGuards(JwtAuthGuard, PermissionGuard)
|
|
292
|
+
export class AdminBlogController {
|
|
293
|
+
@Get()
|
|
294
|
+
@RequirePermissions(CORE_PERMISSIONS.USER_READ.name) // hoặc truyền trực tiếp 'blog.publish'
|
|
295
|
+
list() {
|
|
296
|
+
return []
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Dynamic module API
|
|
304
|
+
|
|
305
|
+
### `AdminCoreOptions`
|
|
306
|
+
|
|
307
|
+
| Key | Type | Mô tả |
|
|
308
|
+
|-----|------|-------|
|
|
309
|
+
| `jwt.accessSecret` | `string` | JWT signing secret — bắt buộc |
|
|
310
|
+
| `jwt.accessExpiresIn` | `string` | mặc định `'1h'` |
|
|
311
|
+
| `jwt.refreshSecret` | `string` | secret cho refresh token |
|
|
312
|
+
| `jwt.refreshExpiresIn` | `string` | mặc định `'30d'` |
|
|
313
|
+
| `rootEmail` | `string` | Email root account tạo tự động khi boot |
|
|
314
|
+
| `rootPassword` | `string` | Mật khẩu root — **bắt buộc khi root chưa tồn tại**, >= 12 ký tự, đọc từ env `ROOT_PASSWORD`. Seeder throw nếu thiếu |
|
|
315
|
+
| `seeder.clientSecrets.webAdmin` | `string` | Secret cho OAuth client `web-admin` — **bắt buộc**, >= 16 ký tự |
|
|
316
|
+
| `seeder.clientSecrets.webPublic` | `string` | Secret cho `web-public` — **bắt buộc** |
|
|
317
|
+
| `seeder.clientSecrets.thirdPartyDemo` | `string` | Secret cho `third-party-demo` — **bắt buộc** |
|
|
318
|
+
| `seeder.clientSecrets.apiService` | `string` | Secret cho `api-service` — **bắt buộc** |
|
|
319
|
+
| `oauth.google` | `{ clientId, clientSecret, callbackURL }` | Config OAuth Google (optional) |
|
|
320
|
+
| `oauth.github` | tương tự | Config GitHub |
|
|
321
|
+
| `oauth.facebook` | `{ appId, appSecret, callbackURL }` | Config Facebook |
|
|
322
|
+
| `seeder.adminRedirectUris` | `string[]` | Redirect URIs cho OAuth `web-admin` client |
|
|
323
|
+
| `seeder.webRedirectUris` | `string[]` | cho `web-public` |
|
|
324
|
+
| `seeder.mobileRedirectUris` | `string[]` | cho `mobile-app` |
|
|
325
|
+
| `extraPermissions` | `PermissionDef[]` | Permissions domain của bạn, merge vào core khi seed |
|
|
326
|
+
| `extraRoles` | `RoleDef[]` | Roles tuỳ biến, merge vào core. Dùng `permissions: '*'` để gán tất cả |
|
|
327
|
+
|
|
328
|
+
### Core defaults
|
|
329
|
+
|
|
330
|
+
11 core permissions built-in (luôn có sau khi seed):
|
|
331
|
+
|
|
332
|
+
- `user.read`, `user.create`, `user.update`, `user.delete`
|
|
333
|
+
- `role.read`, `role.create`, `role.update`, `role.delete`
|
|
334
|
+
- `admin.dashboard`, `admin.settings`, `admin.analytics`
|
|
335
|
+
|
|
336
|
+
3 core roles:
|
|
337
|
+
|
|
338
|
+
- `super-admin` — tất cả permissions (core + extras)
|
|
339
|
+
- `admin` — 11 core permissions
|
|
340
|
+
- `viewer` — read-only: `user.read`, `role.read`, `admin.dashboard`
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Gotchas — lỗi hay gặp
|
|
345
|
+
|
|
346
|
+
### 1. `Cannot find module '@udhuong/admin-core/server'`
|
|
347
|
+
NestJS tsconfig chưa dùng `moduleResolution: node16`. Đổi cả `module` và `moduleResolution` thành `node16`.
|
|
348
|
+
|
|
349
|
+
### 2. `Data type "timestamp" is not supported by "better-sqlite3"`
|
|
350
|
+
Entities trong admin-core dùng Postgres-specific types. Dùng `pg` driver.
|
|
351
|
+
|
|
352
|
+
### 3. CORS bị block ở client
|
|
353
|
+
Server phải `app.enableCors({ origin: CLIENT_URL, credentials: true })`.
|
|
354
|
+
|
|
355
|
+
### 4. Login trả `Invalid client`
|
|
356
|
+
OAuth clients chưa seed. Import `OAuthSeederModule` vào `AppModule` và đảm bảo đã cấu hình `seeder.clientSecrets`. Login với `clientCode` + `clientSecret` do **bạn tự set qua env var**:
|
|
357
|
+
```json
|
|
358
|
+
{"email":"...","password":"...","clientCode":"web-admin","clientSecret":"<OAUTH_WEB_ADMIN_SECRET>"}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### 4a. Seeder throw `Thiếu hoặc quá ngắn (<16 ký tự) client secrets`
|
|
362
|
+
Bạn chưa cấu hình env vars `OAUTH_*_SECRET` hoặc truyền chúng qua `seeder.clientSecrets` trong `forRootAsync`. Sinh secret: `openssl rand -base64 32`. Tuyệt đối không dùng giá trị mẫu trong docs/tests cho production.
|
|
363
|
+
|
|
364
|
+
### 4b. `RootAccountService` throw `Thiếu hoặc quá ngắn rootPassword`
|
|
365
|
+
Phải cung cấp `rootPassword` qua config hoặc env `ROOT_PASSWORD` (>= 12 ký tự) khi boot lần đầu. Sau khi root account đã tạo, có thể bỏ nếu muốn.
|
|
366
|
+
|
|
367
|
+
### 5. Tailwind chỉ ship vài KB, UI vỡ
|
|
368
|
+
`@source` trong CSS của package không quét được consumer source. Viết local `globals.css` với `@source` trỏ cả `../src/**/*.{ts,tsx}` và `../node_modules/@udhuong/admin-core/dist/client/**/*.{js,d.ts}`.
|
|
369
|
+
|
|
370
|
+
### 6. SSR hydration warning với `data-theme`
|
|
371
|
+
Thêm `suppressHydrationWarning` vào `<html>` và `<body>` trong `RootLayout`.
|
|
372
|
+
|
|
373
|
+
### 7. `UserSettingsPage` không còn
|
|
374
|
+
Đã rename thành `UserPreferencesPage`. `SettingsPage` (system-wide settings) đã bị xoá khỏi package — tự implement theo nhu cầu.
|
|
375
|
+
|
|
376
|
+
### 8. Next.js cần `transpilePackages`
|
|
377
|
+
Vì package ship raw CSS: `transpilePackages: ['@udhuong/admin-core']` trong `next.config.ts`.
|
|
378
|
+
|
|
379
|
+
### 9. Consumer chỉ dùng client vẫn bị cảnh báo `@nestjs/*`
|
|
380
|
+
Chạy `npm install` với npm ≥ 7 — peer deps được mark optional qua `peerDependenciesMeta`, không cần cài.
|
|
381
|
+
|
|
382
|
+
### 10. Permissions fetch từ API trả `[]` cho user root
|
|
383
|
+
Root được tạo không tự assign role. Query DB gán role `super-admin` cho user root sau khi seed, hoặc thêm flow assignRootRole trong bootstrap của bạn.
|
|
384
|
+
|
|
385
|
+
### 11. Migration chạy tay trên DB production
|
|
386
|
+
Admin-core đã có migration `CreateOAuthSystem` trong `dist/server/auth/infrastructure/database/postgresql/migrations/`. TypeORM CLI:
|
|
387
|
+
```ts
|
|
388
|
+
migrations: ['node_modules/@udhuong/admin-core/dist/server/**/migrations/*.js']
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Example project
|
|
394
|
+
|
|
395
|
+
Xem full setup client + server tại [admin-core-example](https://github.com/ungdinhhuong/admin-core-example) (sẽ publish riêng), hoặc tham khảo integration-test snapshot trong `/examples` folder của repo.
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Support
|
|
400
|
+
|
|
401
|
+
Issues: https://github.com/ungdinhhuong/admin-core/issues
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## License
|
|
406
|
+
|
|
407
|
+
MIT © 2026 [udhuong](mailto:l19htm4n@gmail.com)
|
package/package.json
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@udhuong/admin-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reusable admin core (client + server) — auth, RBAC, OAuth, user management. Built với NestJS + Next.js, ship trong 1 package với subpath exports.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "udhuong <l19htm4n@gmail.com>",
|
|
7
|
+
"homepage": "https://github.com/ungdinhhuong/admin-core#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ungdinhhuong/admin-core.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ungdinhhuong/admin-core/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"admin",
|
|
17
|
+
"admin-panel",
|
|
18
|
+
"nestjs",
|
|
19
|
+
"nextjs",
|
|
20
|
+
"react",
|
|
21
|
+
"auth",
|
|
22
|
+
"authentication",
|
|
23
|
+
"authorization",
|
|
24
|
+
"rbac",
|
|
25
|
+
"oauth",
|
|
26
|
+
"oauth2",
|
|
27
|
+
"jwt",
|
|
28
|
+
"2fa",
|
|
29
|
+
"totp",
|
|
30
|
+
"user-management"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"sideEffects": [
|
|
36
|
+
"**/*.css"
|
|
37
|
+
],
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"src/client/styles/globals.css",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"types": "./dist/client/index.d.ts",
|
|
47
|
+
"import": "./dist/client/index.js",
|
|
48
|
+
"default": "./dist/client/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./server": {
|
|
51
|
+
"types": "./dist/server/index.d.ts",
|
|
52
|
+
"require": "./dist/server/index.js",
|
|
53
|
+
"default": "./dist/server/index.js"
|
|
54
|
+
},
|
|
55
|
+
"./styles": "./src/client/styles/globals.css",
|
|
56
|
+
"./tailwind-preset": {
|
|
57
|
+
"types": "./dist/tailwind.preset.d.ts",
|
|
58
|
+
"default": "./dist/tailwind.preset.js"
|
|
59
|
+
},
|
|
60
|
+
"./package.json": "./package.json"
|
|
61
|
+
},
|
|
62
|
+
"main": "./dist/client/index.js",
|
|
63
|
+
"types": "./dist/client/index.d.ts",
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "npm run build:client && npm run build:server && npm run build:preset",
|
|
66
|
+
"build:client": "tsc -p tsconfig.client.json",
|
|
67
|
+
"build:server": "tsc -p tsconfig.server.json",
|
|
68
|
+
"build:preset": "tsc tailwind.preset.ts --outDir dist --declaration --module commonjs --esModuleInterop --target ES2020 --skipLibCheck",
|
|
69
|
+
"type-check": "tsc -p tsconfig.client.json --noEmit && tsc -p tsconfig.server.json --noEmit",
|
|
70
|
+
"clean": "rm -rf dist"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@hookform/resolvers": "^5.2.2",
|
|
74
|
+
"@tanstack/react-query": "^5.90.7",
|
|
75
|
+
"axios": "^1.13.2",
|
|
76
|
+
"bcrypt": "^6.0.0",
|
|
77
|
+
"class-transformer": "^0.5.1",
|
|
78
|
+
"class-validator": "^0.14.1",
|
|
79
|
+
"clsx": "^2.1.1",
|
|
80
|
+
"google-auth-library": "^10.5.0",
|
|
81
|
+
"lucide-react": "^0.552.0",
|
|
82
|
+
"next-themes": "^0.4.6",
|
|
83
|
+
"passport": "^0.7.0",
|
|
84
|
+
"passport-facebook": "^3.0.0",
|
|
85
|
+
"passport-github2": "^0.1.12",
|
|
86
|
+
"passport-google-oauth20": "^2.0.0",
|
|
87
|
+
"passport-jwt": "^4.0.1",
|
|
88
|
+
"passport-local": "^1.0.0",
|
|
89
|
+
"qrcode": "^1.5.4",
|
|
90
|
+
"react-hook-form": "^7.72.1",
|
|
91
|
+
"react-hot-toast": "^2.6.0",
|
|
92
|
+
"speakeasy": "^2.0.0",
|
|
93
|
+
"tailwind-merge": "^3.3.1",
|
|
94
|
+
"zod": "^4.3.6"
|
|
95
|
+
},
|
|
96
|
+
"peerDependencies": {
|
|
97
|
+
"@nestjs/common": "^10.0.0",
|
|
98
|
+
"@nestjs/config": "^3.0.0",
|
|
99
|
+
"@nestjs/core": "^10.0.0",
|
|
100
|
+
"@nestjs/event-emitter": "^3.0.0",
|
|
101
|
+
"@nestjs/jwt": "^11.0.0",
|
|
102
|
+
"@nestjs/passport": "^11.0.0",
|
|
103
|
+
"@nestjs/schedule": "^6.0.0",
|
|
104
|
+
"@nestjs/typeorm": "^10.0.0",
|
|
105
|
+
"next": ">=15.0.0",
|
|
106
|
+
"react": ">=18.0.0",
|
|
107
|
+
"react-dom": ">=18.0.0",
|
|
108
|
+
"reflect-metadata": ">=0.1.0",
|
|
109
|
+
"rxjs": ">=7.0.0",
|
|
110
|
+
"typeorm": "^0.3.0"
|
|
111
|
+
},
|
|
112
|
+
"peerDependenciesMeta": {
|
|
113
|
+
"@nestjs/common": { "optional": true },
|
|
114
|
+
"@nestjs/config": { "optional": true },
|
|
115
|
+
"@nestjs/core": { "optional": true },
|
|
116
|
+
"@nestjs/event-emitter": { "optional": true },
|
|
117
|
+
"@nestjs/jwt": { "optional": true },
|
|
118
|
+
"@nestjs/passport": { "optional": true },
|
|
119
|
+
"@nestjs/schedule": { "optional": true },
|
|
120
|
+
"@nestjs/typeorm": { "optional": true },
|
|
121
|
+
"next": { "optional": true },
|
|
122
|
+
"react": { "optional": true },
|
|
123
|
+
"react-dom": { "optional": true },
|
|
124
|
+
"reflect-metadata": { "optional": true },
|
|
125
|
+
"rxjs": { "optional": true },
|
|
126
|
+
"typeorm": { "optional": true }
|
|
127
|
+
},
|
|
128
|
+
"devDependencies": {
|
|
129
|
+
"@nestjs/common": "^10.4.8",
|
|
130
|
+
"@nestjs/config": "^3.3.0",
|
|
131
|
+
"@nestjs/core": "^10.4.8",
|
|
132
|
+
"@nestjs/event-emitter": "^3.0.1",
|
|
133
|
+
"@nestjs/jwt": "^11.0.1",
|
|
134
|
+
"@nestjs/passport": "^11.0.5",
|
|
135
|
+
"@nestjs/schedule": "^6.1.0",
|
|
136
|
+
"@nestjs/typeorm": "^10.0.2",
|
|
137
|
+
"@types/bcrypt": "^6.0.0",
|
|
138
|
+
"@types/node": "^22",
|
|
139
|
+
"@types/passport-facebook": "^3.0.4",
|
|
140
|
+
"@types/passport-github2": "^1.2.9",
|
|
141
|
+
"@types/passport-google-oauth20": "^2.0.17",
|
|
142
|
+
"@types/passport-jwt": "^4.0.1",
|
|
143
|
+
"@types/passport-local": "^1.0.38",
|
|
144
|
+
"@types/qrcode": "^1.5.6",
|
|
145
|
+
"@types/react": "^19",
|
|
146
|
+
"@types/react-dom": "^19",
|
|
147
|
+
"@types/speakeasy": "^2.0.10",
|
|
148
|
+
"daisyui": "^5.4.7",
|
|
149
|
+
"next": "^16.0.7",
|
|
150
|
+
"react": "^19.0.0",
|
|
151
|
+
"react-dom": "^19.0.0",
|
|
152
|
+
"reflect-metadata": "^0.2.2",
|
|
153
|
+
"rxjs": "^7.8.1",
|
|
154
|
+
"tailwindcss": "^4.0.0",
|
|
155
|
+
"typeorm": "^0.3.27",
|
|
156
|
+
"typescript": "^5.7.2"
|
|
157
|
+
},
|
|
158
|
+
"publishConfig": {
|
|
159
|
+
"access": "public"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@source "../";
|
|
3
|
+
@plugin "daisyui";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
html {
|
|
7
|
+
transition: color 200ms, background-color 200ms;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
margin: 0;
|
|
12
|
+
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
13
|
+
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
|
14
|
+
"Helvetica Neue", sans-serif;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
*, ::before, ::after {
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* DaisyUI input focus customization */
|
|
22
|
+
.input,
|
|
23
|
+
.textarea,
|
|
24
|
+
.select,
|
|
25
|
+
.input:focus,
|
|
26
|
+
.textarea:focus,
|
|
27
|
+
.select:focus {
|
|
28
|
+
outline: none;
|
|
29
|
+
outline-offset: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
:root {
|
|
33
|
+
/* Light Theme Colors */
|
|
34
|
+
--color-primary: #10172c;
|
|
35
|
+
--color-secondary: #1f2a44;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
[data-theme="dark"] {
|
|
39
|
+
/* Dark Theme Colors */
|
|
40
|
+
--color-primary: #4a8eff;
|
|
41
|
+
--color-secondary: #1f3a93;
|
|
42
|
+
}
|