@udhuong/admin-core
v0.1.5
Published
Reusable admin core (client + server) — auth, RBAC, OAuth, user management. Built với NestJS + Next.js, ship trong 1 package với subpath exports.
Maintainers
Readme
@udhuong/admin-core
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.
Tính năng
- 🔐 Auth — email/password + JWT (access + refresh) + 2FA/TOTP
- 👥 User management — CRUD user, session, preferences (theme/language/notifications)
- 🛡️ RBAC dynamic — core permissions mặc định + consumer extend
extraPermissions/extraRolesquaforRoot() - 🔗 OAuth 2.0 — client credentials, authorization code, refresh token; social login Google/GitHub/Facebook
- 📧 Mail events — verification, password reset, welcome (consumer tự cài mail sender)
- 🎨 UI ready-to-use —
<LoginPage />,<AdminLayout />,<UsersPage />,<RolesPage />,<PermissionsPage />… với Tailwind v4 + DaisyUI - 🧰 NestJS module —
AdminCoreModule.forRoot()/forRootAsync()(global), auto-seed nếu importOAuthSeederModule
Install
npm install @udhuong/admin-corePeer dependencies (optional theo phía bạn dùng)
Client (Next.js):
npm install next react react-dom
npm install --save-dev tailwindcss @tailwindcss/postcss daisyuiServer (NestJS):
npm install @nestjs/common @nestjs/core @nestjs/config @nestjs/event-emitter \
@nestjs/jwt @nestjs/passport @nestjs/schedule @nestjs/typeorm \
typeorm pg reflect-metadata rxjsTấ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).
Quick start — Client (Next.js 16 App Router)
1. Wrap providers
// src/app/providers.tsx
'use client'
import { CoreProviders } from '@udhuong/admin-core'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<CoreProviders
config={{
apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api',
tokenStorageKey: 'admin_token',
routes: {
LOGIN: '/login',
DASHBOARD: '/dashboard',
UNAUTHORIZED: '/unauthorized',
CHANGE_PASSWORD: '/change-password',
},
}}
>
{children}
</CoreProviders>
)
}CoreProviders đã bao gồm QueryClientProvider, ThemeProvider, AuthProvider, AdminConfigProvider, Toaster. Không wrap lại các provider này bên ngoài.
2. Setup styles (Tailwind v4 + DaisyUI)
Tạo src/app/globals.css:
@import "tailwindcss";
@source "../../src/**/*.{ts,tsx}";
@source "../../node_modules/@udhuong/admin-core/dist/client/**/*.{js,d.ts}";
@plugin "daisyui" {
themes: light --default, dark --prefersdark;
}postcss.config.js:
module.exports = {
plugins: { '@tailwindcss/postcss': {} },
}next.config.ts:
export default {
transpilePackages: ['@udhuong/admin-core'],
}Import vào layout:
// src/app/layout.tsx
import './globals.css'
import { Providers } from './providers'3. Dùng các trang có sẵn
// src/app/login/page.tsx
import { LoginPage } from '@udhuong/admin-core'
export default LoginPage
// src/app/(admin)/users/page.tsx
import { UsersPage } from '@udhuong/admin-core'
export default UsersPageCác trang khác: RolesPage, PermissionsPage, ProfilePage, UserPreferencesPage, ChangePasswordPage, ForgotPasswordPage, ResetPasswordPage, VerifyEmailPage, UnauthorizedPage, UserDetailPage.
4. Dùng hooks
'use client'
import { useAuth } from '@udhuong/admin-core'
export default function Dashboard() {
const { user, logout, isAuthenticated } = useAuth()
if (!isAuthenticated) return <a href="/login">Login</a>
return <div>Hi {user?.displayName} <button onClick={logout}>Logout</button></div>
}Quick start — Server (NestJS 10)
1. Chuẩn bị env vars (BẮT BUỘC)
Trước khi boot, cấu hình .env:
# Root account — password >= 12 ký tự
[email protected]
ROOT_PASSWORD=your-strong-root-password
# OAuth client secrets — mỗi secret >= 16 ký tự
# Sinh: openssl rand -base64 32
OAUTH_WEB_ADMIN_SECRET=<random-32-bytes-base64>
OAUTH_WEB_PUBLIC_SECRET=<random-32-bytes-base64>
OAUTH_THIRD_PARTY_DEMO_SECRET=<random-32-bytes-base64>
OAUTH_API_SERVICE_SECRET=<random-32-bytes-base64>
JWT_ACCESS_SECRET=<random-long-string>2. Wire AdminCoreModule + seeder
// src/app.module.ts
import 'reflect-metadata'
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { EventEmitterModule } from '@nestjs/event-emitter'
import { TypeOrmModule } from '@nestjs/typeorm'
import {
AdminCoreModule,
OAuthSeederModule,
PermissionDef,
RoleDef,
} from '@udhuong/admin-core/server'
import { join } from 'path'
// Extra permissions + roles riêng của app bạn
const EXTRA_PERMISSIONS: PermissionDef[] = [
{ name: 'blog.publish', resource: 'blog', action: 'publish', description: 'Publish blog posts' },
]
const EXTRA_ROLES: RoleDef[] = [
{ name: 'editor', description: 'Blog editor', permissions: ['blog.publish', 'user.read'] },
]
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
EventEmitterModule.forRoot(),
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'myapp',
autoLoadEntities: true,
synchronize: true, // Chỉ dev, production dùng migrations
// Entities từ admin-core
entities: [
join(__dirname, '../node_modules/@udhuong/admin-core/dist/server/**/*.entity.js'),
],
}),
AdminCoreModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
jwt: {
accessSecret: config.get('JWT_ACCESS_SECRET')!,
accessExpiresIn: '1h',
},
rootEmail: config.get('ROOT_EMAIL')!,
rootPassword: config.get('ROOT_PASSWORD')!,
// BẮT BUỘC — seeder sẽ throw nếu thiếu hoặc <16 ký tự
seeder: {
clientSecrets: {
webAdmin: config.get('OAUTH_WEB_ADMIN_SECRET')!,
webPublic: config.get('OAUTH_WEB_PUBLIC_SECRET')!,
thirdPartyDemo: config.get('OAUTH_THIRD_PARTY_DEMO_SECRET')!,
apiService: config.get('OAUTH_API_SERVICE_SECRET')!,
},
},
oauth: {
google: {
clientId: config.get('GOOGLE_CLIENT_ID')!,
clientSecret: config.get('GOOGLE_CLIENT_SECRET')!,
callbackURL: 'http://localhost:4000/api/auth/google/callback',
},
// github, facebook tương tự
},
extraPermissions: EXTRA_PERMISSIONS,
extraRoles: EXTRA_ROLES,
}),
}),
// Seed permissions + roles + OAuth scopes + clients khi boot
OAuthSeederModule,
],
})
export class AppModule {}2. Enable CORS + bootstrap
// src/main.ts
import 'reflect-metadata'
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.enableCors({
origin: process.env.CLIENT_URL || 'http://localhost:3000',
credentials: true,
})
app.setGlobalPrefix('api')
await app.listen(4000)
}
bootstrap()3. tsconfig.json
{
"compilerOptions": {
"module": "node16",
"moduleResolution": "node16",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2023"
}
}4. Protect endpoint với permission
import { Controller, Get, UseGuards } from '@nestjs/common'
import {
JwtAuthGuard,
PermissionGuard,
RequirePermissions,
CORE_PERMISSIONS,
} from '@udhuong/admin-core/server'
@Controller('admin/blog')
@UseGuards(JwtAuthGuard, PermissionGuard)
export class AdminBlogController {
@Get()
@RequirePermissions(CORE_PERMISSIONS.USER_READ.name) // hoặc truyền trực tiếp 'blog.publish'
list() {
return []
}
}Dynamic module API
AdminCoreOptions
| Key | Type | Mô tả |
|-----|------|-------|
| jwt.accessSecret | string | JWT signing secret — bắt buộc |
| jwt.accessExpiresIn | string | mặc định '1h' |
| jwt.refreshSecret | string | secret cho refresh token |
| jwt.refreshExpiresIn | string | mặc định '30d' |
| rootEmail | string | Email root account tạo tự động khi boot |
| 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 |
| seeder.clientSecrets.webAdmin | string | Secret cho OAuth client web-admin — bắt buộc, >= 16 ký tự |
| seeder.clientSecrets.webPublic | string | Secret cho web-public — bắt buộc |
| seeder.clientSecrets.thirdPartyDemo | string | Secret cho third-party-demo — bắt buộc |
| seeder.clientSecrets.apiService | string | Secret cho api-service — bắt buộc |
| oauth.google | { clientId, clientSecret, callbackURL } | Config OAuth Google (optional) |
| oauth.github | tương tự | Config GitHub |
| oauth.facebook | { appId, appSecret, callbackURL } | Config Facebook |
| seeder.adminRedirectUris | string[] | Redirect URIs cho OAuth web-admin client |
| seeder.webRedirectUris | string[] | cho web-public |
| seeder.mobileRedirectUris | string[] | cho mobile-app |
| extraPermissions | PermissionDef[] | Permissions domain của bạn, merge vào core khi seed |
| extraRoles | RoleDef[] | Roles tuỳ biến, merge vào core. Dùng permissions: '*' để gán tất cả |
Core defaults
11 core permissions built-in (luôn có sau khi seed):
user.read,user.create,user.update,user.deleterole.read,role.create,role.update,role.deleteadmin.dashboard,admin.settings,admin.analytics
3 core roles:
super-admin— tất cả permissions (core + extras)admin— 11 core permissionsviewer— read-only:user.read,role.read,admin.dashboard
Gotchas — lỗi hay gặp
1. Cannot find module '@udhuong/admin-core/server'
NestJS tsconfig chưa dùng moduleResolution: node16. Đổi cả module và moduleResolution thành node16.
2. Data type "timestamp" is not supported by "better-sqlite3"
Entities trong admin-core dùng Postgres-specific types. Dùng pg driver.
3. CORS bị block ở client
Server phải app.enableCors({ origin: CLIENT_URL, credentials: true }).
4. Login trả Invalid client
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:
{"email":"...","password":"...","clientCode":"web-admin","clientSecret":"<OAUTH_WEB_ADMIN_SECRET>"}4a. Seeder throw Thiếu hoặc quá ngắn (<16 ký tự) client secrets
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.
4b. RootAccountService throw Thiếu hoặc quá ngắn rootPassword
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.
5. Tailwind chỉ ship vài KB, UI vỡ
@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}.
6. SSR hydration warning với data-theme
Thêm suppressHydrationWarning vào <html> và <body> trong RootLayout.
7. UserSettingsPage không còn
Đã rename thành UserPreferencesPage. SettingsPage (system-wide settings) đã bị xoá khỏi package — tự implement theo nhu cầu.
8. Next.js cần transpilePackages
Vì package ship raw CSS: transpilePackages: ['@udhuong/admin-core'] trong next.config.ts.
9. Consumer chỉ dùng client vẫn bị cảnh báo @nestjs/*
Chạy npm install với npm ≥ 7 — peer deps được mark optional qua peerDependenciesMeta, không cần cài.
10. Permissions fetch từ API trả [] cho user root
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.
11. Migration chạy tay trên DB production
Admin-core đã có migration CreateOAuthSystem trong dist/server/auth/infrastructure/database/postgresql/migrations/. TypeORM CLI:
migrations: ['node_modules/@udhuong/admin-core/dist/server/**/migrations/*.js']Example project
Xem full setup client + server tại admin-core-example (sẽ publish riêng), hoặc tham khảo integration-test snapshot trong /examples folder của repo.
Support
Issues: https://github.com/ungdinhhuong/admin-core/issues
License
MIT © 2026 udhuong
