@infuro/cms-core 1.0.16 → 1.0.18

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.
Files changed (46) hide show
  1. package/README.md +739 -724
  2. package/dist/admin.cjs +1 -1
  3. package/dist/admin.cjs.map +1 -1
  4. package/dist/admin.js +1 -1
  5. package/dist/admin.js.map +1 -1
  6. package/dist/api.cjs +132 -57
  7. package/dist/api.cjs.map +1 -1
  8. package/dist/api.d.cts +1 -1
  9. package/dist/api.d.ts +1 -1
  10. package/dist/api.js +132 -57
  11. package/dist/api.js.map +1 -1
  12. package/dist/auth.cjs.map +1 -1
  13. package/dist/auth.js.map +1 -1
  14. package/dist/cli.cjs +21 -6
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +21 -6
  17. package/dist/cli.js.map +1 -1
  18. package/dist/hooks.cjs.map +1 -1
  19. package/dist/hooks.js.map +1 -1
  20. package/dist/{index-h42MoUNq.d.cts → index-D2C1O9b4.d.cts} +8 -1
  21. package/dist/{index-C85X7cc7.d.ts → index-GMn7-9PX.d.ts} +8 -1
  22. package/dist/index.cjs +135 -57
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +2 -2
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +135 -57
  27. package/dist/index.js.map +1 -1
  28. package/dist/migrations/1772178563554-InitialSchema.ts +304 -304
  29. package/dist/migrations/1772178563555-ChatAndKnowledgeBase.ts +55 -55
  30. package/dist/migrations/1772178563556-KnowledgeBaseVector.ts +16 -16
  31. package/dist/migrations/1774300000000-RbacSeedGroupsAndPermissionUnique.ts +24 -24
  32. package/dist/migrations/1774300000001-SeedAdministratorUsersPermission.ts +35 -35
  33. package/dist/migrations/1774400000000-CustomerAdminAccessContactUser.ts +37 -37
  34. package/dist/migrations/1774400000001-StorefrontCartWishlist.ts +100 -100
  35. package/dist/migrations/1774400000002-WishlistGuestId.ts +29 -29
  36. package/dist/migrations/1774500000000-ProductCollectionHsn.ts +15 -15
  37. package/dist/migrations/1774600000000-OrderKindParentOrderNumber.ts +36 -36
  38. package/dist/migrations/1774800000000-OtpChallengesUserPhone.ts +41 -41
  39. package/dist/migrations/1774900000000-MessageTemplates.ts +39 -39
  40. package/dist/migrations/1775000000000-ProductUomTypeOrderItemSnapshots.ts +29 -29
  41. package/dist/migrations/1775200000000-MediaDriveFolders.ts +38 -38
  42. package/dist/migrations/README.md +3 -3
  43. package/dist/theme.cjs.map +1 -1
  44. package/dist/theme.js.map +1 -1
  45. package/package.json +13 -6
  46. package/src/admin/admin.css +72 -72
package/README.md CHANGED
@@ -1,724 +1,739 @@
1
- # @infuro/cms-core
2
-
3
- A headless CMS framework built on Next.js and TypeORM. It provides a ready-to-use admin panel, CRUD API layer, authentication, plugin system, and UI components — so you only write what's unique to your website.
4
-
5
- ## Overview
6
-
7
- You don't set up or clone core in each website. Install the published package and run the init command to scaffold a new site.
8
-
9
- **Typical workflow for a new site:**
10
-
11
- 1. Create a Next.js app (TypeScript, Tailwind, App Router, `src` directory):
12
- `npx create-next-app@latest my-site --typescript --tailwind --app --src-dir`
13
- 2. From the project root, run:
14
- `npx @infuro/cms-core init`
15
- 3. Copy `.env.example` to `.env`, set `DATABASE_URL`, `NEXTAUTH_SECRET`, `NEXTAUTH_URL`, then run `npm run seed` (or migrations) and `npm run dev`.
16
-
17
- Init creates all required files (data-source, auth-helpers, cms, API routes, admin layout and page, middleware, providers, seed, migration runner, default theme, basic home and contact pages), and can patch `next.config`, `tailwind.config`, layout, and `package.json` and install dependencies. Use `--force` to overwrite existing files, `--dry-run` to see what would be created, `--no-deps` to skip npm install, `--no-patch-config` to skip config changes.
18
-
19
- **Manual setup (if you prefer not to use init):** Install from npm (`npm install @infuro/cms-core typeorm reflect-metadata bcryptjs next-auth next-themes sonner` and `npm install -D @types/node`), then follow the step-by-step setup below. For local development of core itself, use `"@infuro/cms-core": "file:../core"` in the site's package.json.
20
-
21
- ## Project structure (your site after setup)
22
-
23
- ```
24
- your-website/
25
- ├── src/
26
- │ ├── app/
27
- │ │ ├── admin/ # layout.tsx + [[...slug]]/page.tsx
28
- │ │ └── api/
29
- │ │ ├── auth/ # NextAuth route
30
- │ │ └── [[...path]]/ # Catch-all for CMS API
31
- │ ├── lib/
32
- │ │ ├── data-source.ts
33
- │ │ ├── auth-helpers.ts
34
- │ │ ├── cms.ts
35
- │ │ ├── theme-registry.ts
36
- │ │ └── seed.ts
37
- │ ├── themes/ # Optional: default theme from init
38
- │ ├── migrations/
39
- │ ├── middleware.ts
40
- │ └── ... # Your pages, components, etc.
41
- ```
42
-
43
- ## Getting Started
44
-
45
- ### 1. Create a Next.js app
46
-
47
- ```bash
48
- npx create-next-app@latest my-website --typescript --tailwind --app --src-dir
49
- cd my-website
50
- ```
51
-
52
- ### 2. Install the package
53
-
54
- ```bash
55
- npm install @infuro/cms-core typeorm reflect-metadata bcryptjs next-auth next-themes sonner
56
- npm install -D @types/node
57
- ```
58
-
59
- Peer dependencies (Next.js app usually has these): `next` ≥14, `react` ≥18, `react-dom` ≥18, `next-auth` ^4.24. For local development, use `"@infuro/cms-core": "file:../core"` in package.json and run `npm install`.
60
-
61
- ### 3. Configure `next.config.js`
62
-
63
- ```js
64
- const nextConfig = {
65
- reactStrictMode: false,
66
- serverExternalPackages: ['@infuro/cms-core', 'typeorm'],
67
- };
68
- module.exports = nextConfig;
69
- ```
70
-
71
- `serverExternalPackages` is required so TypeORM decorators and reflect-metadata work correctly on the server.
72
-
73
- ### 4. Set up the database
74
-
75
- Create a `.env` file:
76
-
77
- ```env
78
- DATABASE_URL=postgres://user:password@localhost:5432/mydb
79
- NEXTAUTH_SECRET=your-random-secret
80
- NEXTAUTH_URL=http://localhost:3000
81
- ```
82
-
83
- Create `src/lib/data-source.ts`:
84
-
85
- ```ts
86
- import 'reflect-metadata';
87
- import { DataSource } from 'typeorm';
88
- import { CMS_ENTITY_MAP } from '@infuro/cms-core';
89
-
90
- let dataSource: DataSource | null = null;
91
-
92
- export function getDataSource(): DataSource {
93
- if (!dataSource) {
94
- dataSource = new DataSource({
95
- type: 'postgres',
96
- url: process.env.DATABASE_URL,
97
- entities: Object.values(CMS_ENTITY_MAP),
98
- synchronize: false,
99
- });
100
- }
101
- return dataSource;
102
- }
103
-
104
- export async function getDataSourceInitialized(): Promise<DataSource> {
105
- const ds = getDataSource();
106
- if (!ds.isInitialized) await ds.initialize();
107
- return ds;
108
- }
109
- ```
110
-
111
- > **Note:** `synchronize: false` — use TypeORM migrations (see [Migrations](#migrations)).
112
-
113
- ### 5. Set up auth helpers
114
-
115
- Create `src/lib/auth-helpers.ts`:
116
-
117
- ```ts
118
- import { getServerSession } from 'next-auth';
119
- import { NextResponse } from 'next/server';
120
- import { createAuthHelpers } from '@infuro/cms-core/auth';
121
-
122
- const helpers = createAuthHelpers(
123
- async () => {
124
- const s = await getServerSession();
125
- return s ? { user: s.user } : null;
126
- },
127
- NextResponse
128
- );
129
-
130
- export const requireAuth = helpers.requireAuth;
131
- export const requirePermission = helpers.requirePermission;
132
- export const getAuthenticatedUser = helpers.getAuthenticatedUser;
133
- ```
134
-
135
- ### 6. Set up CMS with plugins
136
-
137
- Create `src/lib/cms.ts`:
138
-
139
- ```ts
140
- import {
141
- createCmsApp,
142
- localStoragePlugin,
143
- type CmsApp,
144
- } from '@infuro/cms-core';
145
- import { getDataSourceInitialized } from './data-source';
146
-
147
- let cmsPromise: Promise<CmsApp> | null = null;
148
-
149
- export async function getCms(): Promise<CmsApp> {
150
- if (cmsPromise) return cmsPromise;
151
- const dataSource = await getDataSourceInitialized();
152
- cmsPromise = createCmsApp({
153
- dataSource,
154
- config: process.env as unknown as Record<string, string>,
155
- plugins: [
156
- localStoragePlugin({ dir: 'public/uploads' }),
157
- // Add more: emailPlugin({...}), analyticsPlugin({...}), etc.
158
- ],
159
- });
160
- return cmsPromise;
161
- }
162
- ```
163
-
164
- ### 7. Mount the API
165
-
166
- Create `src/app/api/[[...path]]/route.ts`:
167
-
168
- ```ts
169
- import { NextResponse } from 'next/server';
170
- import { getServerSession } from 'next-auth';
171
- import { createCmsApiHandler } from '@infuro/cms-core/api';
172
- import { CMS_ENTITY_MAP } from '@infuro/cms-core';
173
- import { getDataSourceInitialized } from '@/lib/data-source';
174
- import { requireAuth } from '@/lib/auth-helpers';
175
- import { getCms } from '@/lib/cms';
176
- import bcrypt from 'bcryptjs';
177
-
178
- const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
179
-
180
- let handlerPromise: Promise<ReturnType<typeof createCmsApiHandler>> | null = null;
181
-
182
- async function getHandler() {
183
- if (!handlerPromise) {
184
- const dataSource = await getDataSourceInitialized();
185
- handlerPromise = Promise.resolve(
186
- createCmsApiHandler({
187
- dataSource,
188
- entityMap: CMS_ENTITY_MAP,
189
- requireAuth,
190
- json: NextResponse.json.bind(NextResponse),
191
- getCms,
192
- userAuth: {
193
- dataSource,
194
- entityMap: CMS_ENTITY_MAP,
195
- json: NextResponse.json.bind(NextResponse),
196
- baseUrl,
197
- hashPassword: (p) => Promise.resolve(bcrypt.hashSync(p, 12)),
198
- comparePassword: (p, h) => Promise.resolve(bcrypt.compareSync(p, h)),
199
- resetExpiryHours: 1,
200
- getSession: () =>
201
- getServerSession().then((s) => (s ? { user: s.user } : null)),
202
- },
203
- dashboard: {
204
- dataSource,
205
- entityMap: CMS_ENTITY_MAP,
206
- json: NextResponse.json.bind(NextResponse),
207
- requireAuth,
208
- requirePermission: requireAuth,
209
- },
210
- upload: {
211
- json: NextResponse.json.bind(NextResponse),
212
- requireAuth,
213
- storage: () => getCms().then((cms) => cms.getPlugin('storage')),
214
- localUploadDir: 'public/uploads',
215
- },
216
- blogBySlug: {
217
- dataSource,
218
- entityMap: CMS_ENTITY_MAP,
219
- json: NextResponse.json.bind(NextResponse),
220
- requireAuth: async () => null,
221
- },
222
- formBySlug: {
223
- dataSource,
224
- entityMap: CMS_ENTITY_MAP,
225
- json: NextResponse.json.bind(NextResponse),
226
- requireAuth: async () => null,
227
- },
228
- usersApi: {
229
- dataSource,
230
- entityMap: CMS_ENTITY_MAP,
231
- json: NextResponse.json.bind(NextResponse),
232
- requireAuth,
233
- baseUrl,
234
- },
235
- })
236
- );
237
- }
238
- return handlerPromise;
239
- }
240
-
241
- async function handle(method: string, req: Request, context: { params: Promise<{ path?: string[] }> }) {
242
- try {
243
- const handler = await getHandler();
244
- const { path = [] } = await context.params;
245
- return handler.handle(method, path, req);
246
- } catch {
247
- return NextResponse.json({ error: 'Server Error' }, { status: 500 });
248
- }
249
- }
250
-
251
- export async function GET(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('GET', req, ctx); }
252
- export async function POST(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('POST', req, ctx); }
253
- export async function PUT(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('PUT', req, ctx); }
254
- export async function PATCH(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('PATCH', req, ctx); }
255
- export async function DELETE(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('DELETE', req, ctx); }
256
- ```
257
-
258
- ### 8. Mount NextAuth
259
-
260
- Create `src/app/api/auth/[...nextauth]/route.ts`:
261
-
262
- ```ts
263
- import NextAuth from 'next-auth';
264
- import { getNextAuthOptions } from '@infuro/cms-core/auth';
265
- import { getDataSourceInitialized } from '@/lib/data-source';
266
- import { CMS_ENTITY_MAP } from '@infuro/cms-core';
267
- import bcrypt from 'bcryptjs';
268
-
269
- async function getOptions() {
270
- const dataSource = await getDataSourceInitialized();
271
- const userRepo = dataSource.getRepository(CMS_ENTITY_MAP.users);
272
- return getNextAuthOptions({
273
- getUserByEmail: async (email: string) => {
274
- return userRepo.findOne({
275
- where: { email },
276
- relations: ['group', 'group.permissions'],
277
- select: ['id', 'email', 'name', 'password', 'blocked', 'deleted', 'groupId'],
278
- }) as any;
279
- },
280
- comparePassword: (plain, hash) => Promise.resolve(bcrypt.compareSync(plain, hash)),
281
- signInPage: '/admin/signin',
282
- });
283
- }
284
-
285
- let handler: ReturnType<typeof NextAuth> | null = null;
286
-
287
- async function getHandler() {
288
- if (!handler) handler = NextAuth(await getOptions());
289
- return handler;
290
- }
291
-
292
- export async function GET(req: Request) {
293
- return ((await getHandler()) as any).GET(req);
294
- }
295
- export async function POST(req: Request) {
296
- return ((await getHandler()) as any).POST(req);
297
- }
298
- ```
299
-
300
- ### 9. Mount the admin panel
301
-
302
- Create `src/app/admin/layout.tsx`:
303
-
304
- ```tsx
305
- 'use client';
306
- import '@infuro/cms-core/admin.css';
307
- import AdminLayout from '@infuro/cms-core/admin';
308
-
309
- export default function AdminLayoutWrapper({ children }: { children: React.ReactNode }) {
310
- return (
311
- <AdminLayout
312
- customNavItems={[]}
313
- customNavSections={[]}
314
- customCrudConfigs={{}}
315
- >
316
- {children}
317
- </AdminLayout>
318
- );
319
- }
320
- ```
321
-
322
- Create `src/app/admin/[[...slug]]/page.tsx`:
323
-
324
- ```tsx
325
- import { AdminPageResolver } from '@infuro/cms-core/admin';
326
-
327
- export default async function AdminPage({ params }: { params: Promise<{ slug?: string[] }> }) {
328
- const { slug } = await params;
329
- return <AdminPageResolver slug={slug} />;
330
- }
331
- ```
332
-
333
- The admin at `/admin` is rendered by the package (layout, sidebar, header, built-in pages). Pass `customNavSections` and `customCrudConfigs` to add your own sidebar links and CRUD list pages (see [Adding custom pages and admin nav](#adding-custom-pages-and-admin-nav)).
334
-
335
- ### 10. Configure Tailwind
336
-
337
- Core's admin components use Tailwind classes. Include the package in `content` so those classes aren't purged:
338
-
339
- ```js
340
- content: [
341
- "./src/**/*.{js,ts,jsx,tsx,mdx}",
342
- // When using from npm:
343
- "./node_modules/@infuro/cms-core/dist/**/*.{js,cjs}",
344
- // When using file:../core (local):
345
- // "../core/src/**/*.{js,ts,jsx,tsx}",
346
- ],
347
- ```
348
-
349
- You also need the shadcn/ui color mappings in `theme.extend.colors` — see the [Tailwind Config](#tailwind-config) section below.
350
-
351
- ### 11. Add middleware
352
-
353
- Create `src/middleware.ts`:
354
-
355
- ```ts
356
- import { NextResponse } from 'next/server';
357
- import type { NextRequest } from 'next/server';
358
- import { createCmsMiddleware } from '@infuro/cms-core/auth';
359
-
360
- const cmsMiddleware = createCmsMiddleware({
361
- // Optional: allow unauthenticated access to specific API paths/methods (e.g. public form submit)
362
- publicApiMethods: {
363
- '/api/contacts': ['POST'],
364
- '/api/form-submissions': ['POST'],
365
- '/api/blogs': ['GET'],
366
- '/api/forms': ['GET'],
367
- '/api/auth': ['GET', 'POST'],
368
- '/api/users/forgot-password': ['POST'],
369
- '/api/users/set-password': ['POST'],
370
- '/api/users/invite': ['POST'],
371
- },
372
- });
373
-
374
- export function middleware(request: NextRequest) {
375
- const result = cmsMiddleware({
376
- nextUrl: request.nextUrl,
377
- url: request.url,
378
- method: request.method,
379
- cookies: request.cookies,
380
- });
381
-
382
- if (result.type === 'next') return NextResponse.next();
383
- if (result.type === 'redirect') return NextResponse.redirect(result.url);
384
- if (result.type === 'json') return NextResponse.json(result.body, { status: result.status });
385
- return NextResponse.next();
386
- }
387
-
388
- export const config = {
389
- matcher: ['/admin/:path*', '/api/:path*'],
390
- };
391
- ```
392
-
393
- ### 12. Add providers
394
-
395
- Wrap your root layout with session and theme providers:
396
-
397
- ```tsx
398
- // src/app/providers.tsx
399
- "use client";
400
- import { ThemeProvider } from "next-themes";
401
- import { SessionProvider } from "next-auth/react";
402
- import { Toaster } from "sonner";
403
-
404
- export function Providers({ children }: { children: React.ReactNode }) {
405
- return (
406
- <SessionProvider>
407
- <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
408
- {children}
409
- <Toaster position="top-right" />
410
- </ThemeProvider>
411
- </SessionProvider>
412
- );
413
- }
414
- ```
415
-
416
- Use it in `src/app/layout.tsx`:
417
-
418
- ```tsx
419
- import { Providers } from './providers';
420
-
421
- export default function RootLayout({ children }: { children: React.ReactNode }) {
422
- return (
423
- <html lang="en">
424
- <body>
425
- <Providers>{children}</Providers>
426
- </body>
427
- </html>
428
- );
429
- }
430
- ```
431
-
432
- ## Adding custom pages and admin nav
433
-
434
- **Custom sidebar links:** Pass `customNavItems` or `customNavSections` to `AdminLayout` in your `src/app/admin/layout.tsx`. Each item has `href`, `label`, and optional `icon`. Use `href` like `/admin/locations` so the link opens under the admin.
435
-
436
- **Custom CRUD (list + optional add/edit):** Define a `CustomCrudConfig` (title, apiEndpoint, columns, addEditPageUrl, optional filters) and pass it as `customCrudConfigs={{ myResource: config }}` to `AdminLayout`. You must have a corresponding API (e.g. under your catch-all or a custom route) and entity. The first path segment (e.g. `locations`) is the key; add a nav item with `href: '/admin/locations'`.
437
-
438
- **Custom full pages:** For a page that isn’t a CRUD list, add a Next.js route under admin, e.g. `src/app/admin/reports/page.tsx`, and render your component there. The admin layout wraps all `/admin/*` routes, so your page appears inside the same shell. Add a link in `customNavItems` or `customNavSections` with `href: '/admin/reports'`.
439
-
440
- Types are exported from `@infuro/cms-core/admin`: `CustomNavItem`, `CustomNavSection`, `CustomCrudConfig`, `CustomCrudColumn`, etc.
441
-
442
- ## Database Setup
443
-
444
- ### First-time setup (quick)
445
-
446
- For initial development, temporarily set `synchronize: true` in your `data-source.ts`, then run the seed script:
447
-
448
- ```bash
449
- npx tsx src/lib/seed.ts
450
- ```
451
-
452
- This creates all tables and inserts default data (admin user, categories, tags, forms). Switch `synchronize` back to `false` afterwards.
453
-
454
- ### Migrations (production)
455
-
456
- TypeORM CLI requires `tsx` and `dotenv/config` to load TypeScript data sources with `.env` support. The `TYPEORM_CLI=1` env var enables the migrations path (kept off at runtime to avoid Next.js loading `.ts` migration files):
457
-
458
- ```bash
459
- # Generate a migration from entity changes
460
- TYPEORM_CLI=1 npx tsx -r dotenv/config node_modules/typeorm/cli.js migration:generate -d src/lib/data-source.ts src/migrations/MyMigration
461
-
462
- # Run pending migrations
463
- TYPEORM_CLI=1 npx tsx -r dotenv/config node_modules/typeorm/cli.js migration:run -d src/lib/data-source.ts
464
-
465
- # Revert last migration
466
- TYPEORM_CLI=1 npx tsx -r dotenv/config node_modules/typeorm/cli.js migration:revert -d src/lib/data-source.ts
467
- ```
468
-
469
- Your `data-source.ts` should conditionally include migrations and export a default:
470
-
471
- ```ts
472
- export function getDataSource(): DataSource {
473
- if (!dataSource) {
474
- dataSource = new DataSource({
475
- type: 'postgres',
476
- url: process.env.DATABASE_URL,
477
- entities: Object.values(CMS_ENTITY_MAP),
478
- synchronize: false,
479
- ...(process.env.TYPEORM_CLI && { migrations: ['src/migrations/*.ts'] }),
480
- });
481
- }
482
- return dataSource;
483
- }
484
-
485
- export default getDataSource();
486
- ```
487
-
488
- ## Core Entities
489
-
490
- | Entity | Table | Purpose |
491
- |--------|-------|---------|
492
- | `User` | `users` | Admin users with groups/permissions |
493
- | `UserGroup` | `user_groups` | Role-based groups |
494
- | `Permission` | `permissions` | Granular permissions |
495
- | `Blog` | `blogs` | Blog posts with slug, SEO, tags, categories |
496
- | `Category` | `categories` | Blog categories |
497
- | `Tag` | `tags` | Blog tags (many-to-many with blogs) |
498
- | `Comment` | `comments` | Blog comments |
499
- | `Contact` | `contacts` | Contact form submissions |
500
- | `Form` | `forms` | Dynamic forms |
501
- | `FormField` | `form_fields` | Form field definitions |
502
- | `FormSubmission` | `form_submissions` | Form submission data |
503
- | `Seo` | `seos` | SEO metadata |
504
- | `Config` | `configs` | Key-value configuration |
505
- | `PasswordResetToken` | `password_reset_tokens` | Password reset flow |
506
-
507
- ## API Endpoints
508
-
509
- All mounted under `/api` via the single catch-all route:
510
-
511
- | Endpoint | Methods | Auth | Description |
512
- |----------|---------|------|-------------|
513
- | `/api/{resource}` | GET, POST | Yes | CRUD list/create for any entity in `CMS_ENTITY_MAP` |
514
- | `/api/{resource}/{id}` | GET, PUT, DELETE | Yes | CRUD get/update/delete by ID |
515
- | `/api/blogs/slug/{slug}` | GET | No | Public blog by slug |
516
- | `/api/forms/slug/{slug}` | GET | No | Public form by slug |
517
- | `/api/users` | GET, POST | Yes | User management |
518
- | `/api/users/{id}` | GET, PUT, DELETE | Yes | User by ID |
519
- | `/api/users/forgot-password` | POST | No | Password reset request |
520
- | `/api/users/set-password` | POST | No | Set new password |
521
- | `/api/users/invite` | POST | No | Accept invite |
522
- | `/api/dashboard/stats` | GET | Yes | Dashboard statistics |
523
- | `/api/analytics` | GET | Yes | Analytics data |
524
- | `/api/upload` | POST | Yes | File upload |
525
- | `/api/auth/*` | GET, POST | No | NextAuth routes |
526
-
527
- ## Plugin System
528
-
529
- Plugins are initialized via `createCmsApp` and accessed with `cms.getPlugin('name')`.
530
-
531
- ### Built-in Plugins
532
-
533
- | Plugin | Factory | Purpose |
534
- |--------|---------|---------|
535
- | Storage (S3) | `s3StoragePlugin({...})` | S3 file uploads |
536
- | Storage (Local) | `localStoragePlugin({dir})` | Local file uploads |
537
- | Email | `emailPlugin({type, from, ...})` | Email via SMTP/SES/Gmail |
538
- | Analytics | `analyticsPlugin({...})` | Google Analytics integration |
539
- | ERP | `erpPlugin({...})` | ERP/CRM integration |
540
- | SMS | `smsPlugin({...})` | SMS notifications |
541
- | Payment | `paymentPlugin({...})` | Payment processing |
542
-
543
- ### Custom Plugins
544
-
545
- Implement the `CmsPlugin` interface:
546
-
547
- ```ts
548
- import type { CmsPlugin, PluginContext } from '@infuro/cms-core';
549
-
550
- export const myPlugin: CmsPlugin<MyService> = {
551
- name: 'my-plugin',
552
- version: '1.0.0',
553
- async init(context: PluginContext) {
554
- return new MyService(context.config);
555
- },
556
- };
557
- ```
558
-
559
- Register it in `cms.ts`:
560
-
561
- ```ts
562
- plugins: [
563
- localStoragePlugin({ dir: 'public/uploads' }),
564
- myPlugin,
565
- ],
566
- ```
567
-
568
- Access it anywhere:
569
-
570
- ```ts
571
- const cms = await getCms();
572
- const service = cms.getPlugin<MyService>('my-plugin');
573
- ```
574
-
575
- ## Package Exports
576
-
577
- | Import Path | Contents |
578
- |-------------|----------|
579
- | `@infuro/cms-core` | Entities, plugins, registry, utilities |
580
- | `@infuro/cms-core/api` | `createCmsApiHandler`, CRUD handlers, auth handlers |
581
- | `@infuro/cms-core/auth` | `createAuthHelpers`, `createCmsMiddleware`, `getNextAuthOptions` |
582
- | `@infuro/cms-core/admin` | Admin layout, pages, components (React, `'use client'`) |
583
- | `@infuro/cms-core/hooks` | `useIsMobile`, `useAnalytics`, `usePlugin` |
584
-
585
- ## Extending
586
-
587
- ### Adding custom entities
588
-
589
- 1. Define your TypeORM entity
590
- 2. Add it to a merged entity map:
591
-
592
- ```ts
593
- import { CMS_ENTITY_MAP } from '@infuro/cms-core';
594
- import { Product } from './entities/product.entity';
595
-
596
- const ENTITY_MAP = { ...CMS_ENTITY_MAP, products: Product };
597
- ```
598
-
599
- 3. Pass the merged map to `getDataSource()` entities and `createCmsApiHandler({ entityMap })`
600
-
601
- ### Adding custom API routes
602
-
603
- Add files alongside the catch-all (e.g. `src/app/api/my-custom/route.ts`). Next.js resolves specific routes before the catch-all.
604
-
605
- ### Customizing middleware
606
-
607
- Pass config to `createCmsMiddleware()`:
608
-
609
- ```ts
610
- createCmsMiddleware({
611
- publicAdminPaths: ['/admin/signin', '/admin/custom-public-page'],
612
- publicApiMethods: {
613
- '/api/products': ['GET'],
614
- },
615
- });
616
- ```
617
-
618
- ## Tailwind Config
619
-
620
- The admin panel and UI components use shadcn/ui and require CSS variable-based color mappings. Your `tailwind.config.js` needs these in `theme.extend.colors`:
621
-
622
- ```js
623
- module.exports = {
624
- content: [
625
- "./src/**/*.{js,ts,jsx,tsx,mdx}",
626
- "./node_modules/@infuro/cms-core/dist/**/*.{js,cjs}",
627
- ],
628
- darkMode: "class",
629
- theme: {
630
- extend: {
631
- colors: {
632
- background: "hsl(var(--background))",
633
- foreground: "hsl(var(--foreground))",
634
- card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
635
- popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))" },
636
- primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
637
- secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
638
- muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
639
- accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
640
- destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
641
- border: "hsl(var(--border))",
642
- input: "hsl(var(--input))",
643
- ring: "hsl(var(--ring))",
644
- sidebar: {
645
- DEFAULT: "hsl(var(--sidebar-background))",
646
- foreground: "hsl(var(--sidebar-foreground))",
647
- primary: "hsl(var(--sidebar-primary))",
648
- "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
649
- accent: "hsl(var(--sidebar-accent))",
650
- "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
651
- border: "hsl(var(--sidebar-border))",
652
- ring: "hsl(var(--sidebar-ring))",
653
- },
654
- },
655
- borderRadius: {
656
- lg: "var(--radius)",
657
- md: "calc(var(--radius) - 2px)",
658
- sm: "calc(var(--radius) - 4px)",
659
- },
660
- },
661
- },
662
- plugins: [require("tailwindcss-animate")],
663
- };
664
- ```
665
-
666
- The CSS variables are injected by the admin layout at runtime. Your website's own CSS can also define them in `:root` if your public pages use shadcn/ui components.
667
-
668
- ## Environment Variables
669
-
670
- | Variable | Required | Description |
671
- |----------|----------|-------------|
672
- | `DATABASE_URL` | Yes | PostgreSQL connection string |
673
- | `NEXTAUTH_SECRET` | Yes | NextAuth JWT secret |
674
- | `NEXTAUTH_URL` | Yes | App base URL |
675
- | `STORAGE_TYPE` | No | `s3` or `local` (default: local) |
676
- | `AWS_BUCKET_NAME` | If S3 | S3 bucket name |
677
- | `AWS_REGION` | If S3/SES | AWS region |
678
- | `AWS_ACCESS_KEY_ID` | If S3/SES | AWS access key |
679
- | `AWS_SECRET_ACCESS_KEY` | If S3/SES | AWS secret key |
680
- | `SMTP_TYPE` | No | `SMTP`, `AWS`, or `GMAIL` |
681
- | `SMTP_FROM` | If email | Sender email |
682
- | `SMTP_TO` | If email | Default recipient |
683
- | `SMTP_USER` | If SMTP | SMTP username |
684
- | `SMTP_PASSWORD` | If SMTP | SMTP password |
685
- | `GOOGLE_ANALYTICS_PRIVATE_KEY` | If analytics | GA service account key |
686
- | `GOOGLE_ANALYTICS_CLIENT_EMAIL` | If analytics | GA service account email |
687
- | `GOOGLE_ANALYTICS_VIEW_ID` | If analytics | GA property/view ID |
688
-
689
- ## Development
690
-
691
- ### Quick start (existing website using core)
692
-
693
- ```bash
694
- # 1. Build core (once, or use watch mode)
695
- cd core
696
- npm run build
697
-
698
- # 2. Install website dependencies (links core via file:../core)
699
- cd ../my-website
700
- npm install
701
-
702
- # 3. Set up .env (DATABASE_URL, NEXTAUTH_SECRET, NEXTAUTH_URL)
703
-
704
- # 4. Create tables & seed (set synchronize: true in data-source.ts first)
705
- npx tsx src/lib/seed.ts
706
- # Then set synchronize back to false
707
-
708
- # 5. Start dev server
709
- npm run dev
710
- ```
711
-
712
- ### Watch mode (developing core + website simultaneously)
713
-
714
- Terminal 1:
715
- ```bash
716
- cd core && npm run dev
717
- ```
718
-
719
- Terminal 2:
720
- ```bash
721
- cd my-website && npm run dev
722
- ```
723
-
724
- Changes to core are picked up automatically by the website's dev server.
1
+ # @infuro/cms-core
2
+
3
+ A headless CMS framework built on Next.js and TypeORM. It provides a ready-to-use admin panel, CRUD API layer, authentication, plugin system, and UI components — so you only write what's unique to your website.
4
+
5
+ ## Overview
6
+
7
+ You don't set up or clone core in each website. Install the published package and run the init command to scaffold a new site.
8
+
9
+ **Typical workflow for a new site:**
10
+
11
+ 1. Create a Next.js app (TypeScript, Tailwind, App Router, `src` directory):
12
+ `npx create-next-app@latest my-site --typescript --tailwind --app --src-dir`
13
+ 2. From the project root, run:
14
+ `npx @infuro/cms-core init`
15
+ 3. Copy `.env.example` to `.env`, set `DATABASE_URL`, `NEXTAUTH_SECRET`, `NEXTAUTH_URL`, then run `npm run seed` (or migrations) and `npm run dev`.
16
+
17
+ Init creates all required files (data-source, auth-helpers, cms, API routes, admin layout and page, middleware, providers, seed, migration runner, default theme, basic home and contact pages), and can patch `next.config`, `tailwind.config`, layout, and `package.json` and install dependencies. Use `--force` to overwrite existing files, `--dry-run` to see what would be created, `--no-deps` to skip npm install, `--no-patch-config` to skip config changes.
18
+
19
+ **Manual setup (if you prefer not to use init):** Install from npm (`npm install @infuro/cms-core typeorm reflect-metadata bcryptjs next-auth next-themes sonner` and `npm install -D @types/node`), then follow the step-by-step setup below. For local development of core itself, use `"@infuro/cms-core": "file:../core"` in the site's package.json.
20
+
21
+ ## Project structure (your site after setup)
22
+
23
+ ```
24
+ your-website/
25
+ ├── src/
26
+ │ ├── app/
27
+ │ │ ├── admin/ # layout.tsx + [[...slug]]/page.tsx
28
+ │ │ └── api/
29
+ │ │ ├── auth/ # NextAuth route
30
+ │ │ └── [[...path]]/ # Catch-all for CMS API
31
+ │ ├── lib/
32
+ │ │ ├── data-source.ts
33
+ │ │ ├── auth-helpers.ts
34
+ │ │ ├── cms.ts
35
+ │ │ ├── theme-registry.ts
36
+ │ │ └── seed.ts
37
+ │ ├── themes/ # Optional: default theme from init
38
+ │ ├── migrations/
39
+ │ ├── middleware.ts
40
+ │ └── ... # Your pages, components, etc.
41
+ ```
42
+
43
+ ## Getting Started
44
+
45
+ ### 1. Create a Next.js app
46
+
47
+ ```bash
48
+ npx create-next-app@latest my-website --typescript --tailwind --app --src-dir
49
+ cd my-website
50
+ ```
51
+
52
+ ### 2. Install the package
53
+
54
+ ```bash
55
+ npm install @infuro/cms-core typeorm reflect-metadata bcryptjs next-auth next-themes sonner
56
+ npm install -D @types/node
57
+ ```
58
+
59
+ Peer dependencies (Next.js app usually has these): `next` ≥14, `react` ≥18, `react-dom` ≥18, `next-auth` ^4.24. For local development, use `"@infuro/cms-core": "file:../core"` in package.json and run `npm install`.
60
+
61
+ ### 3. Configure `next.config.js`
62
+
63
+ ```js
64
+ const nextConfig = {
65
+ reactStrictMode: false,
66
+ serverExternalPackages: ['@infuro/cms-core', 'typeorm'],
67
+ };
68
+ module.exports = nextConfig;
69
+ ```
70
+
71
+ `serverExternalPackages` is required so TypeORM decorators and reflect-metadata work correctly on the server.
72
+
73
+ ### 4. Set up the database
74
+
75
+ Create a `.env` file:
76
+
77
+ ```env
78
+ DATABASE_URL=postgres://user:password@localhost:5432/mydb
79
+ NEXTAUTH_SECRET=your-random-secret
80
+ NEXTAUTH_URL=http://localhost:3000
81
+ ```
82
+
83
+ Create `src/lib/data-source.ts`:
84
+
85
+ ```ts
86
+ import 'reflect-metadata';
87
+ import { DataSource } from 'typeorm';
88
+ import { CMS_ENTITY_MAP } from '@infuro/cms-core';
89
+
90
+ let dataSource: DataSource | null = null;
91
+
92
+ export function getDataSource(): DataSource {
93
+ if (!dataSource) {
94
+ dataSource = new DataSource({
95
+ type: 'postgres',
96
+ url: process.env.DATABASE_URL,
97
+ entities: Object.values(CMS_ENTITY_MAP),
98
+ synchronize: false,
99
+ });
100
+ }
101
+ return dataSource;
102
+ }
103
+
104
+ export async function getDataSourceInitialized(): Promise<DataSource> {
105
+ const ds = getDataSource();
106
+ if (!ds.isInitialized) await ds.initialize();
107
+ return ds;
108
+ }
109
+ ```
110
+
111
+ > **Note:** `synchronize: false` — use TypeORM migrations (see [Migrations](#migrations)).
112
+
113
+ ### 5. Set up auth helpers
114
+
115
+ Create `src/lib/auth-helpers.ts`:
116
+
117
+ ```ts
118
+ import { getServerSession } from 'next-auth';
119
+ import { NextResponse } from 'next/server';
120
+ import { createAuthHelpers } from '@infuro/cms-core/auth';
121
+
122
+ const helpers = createAuthHelpers(
123
+ async () => {
124
+ const s = await getServerSession();
125
+ return s ? { user: s.user } : null;
126
+ },
127
+ NextResponse
128
+ );
129
+
130
+ export const requireAuth = helpers.requireAuth;
131
+ export const requirePermission = helpers.requirePermission;
132
+ export const requireEntityPermission = helpers.requireEntityPermission;
133
+ export const requireAdminAccess = helpers.requireAdminAccess;
134
+ export const getAuthenticatedUser = helpers.getAuthenticatedUser;
135
+ ```
136
+
137
+ ### 6. Set up CMS with plugins
138
+
139
+ Create `src/lib/cms.ts`:
140
+
141
+ ```ts
142
+ import {
143
+ createCmsApp,
144
+ localStoragePlugin,
145
+ type CmsApp,
146
+ } from '@infuro/cms-core';
147
+ import { getDataSourceInitialized } from './data-source';
148
+
149
+ let cmsPromise: Promise<CmsApp> | null = null;
150
+
151
+ export async function getCms(): Promise<CmsApp> {
152
+ if (cmsPromise) return cmsPromise;
153
+ const dataSource = await getDataSourceInitialized();
154
+ cmsPromise = createCmsApp({
155
+ dataSource,
156
+ config: process.env as unknown as Record<string, string>,
157
+ plugins: [
158
+ localStoragePlugin({ dir: 'public/uploads' }),
159
+ // Add more: emailPlugin({...}), analyticsPlugin({...}), etc.
160
+ ],
161
+ });
162
+ return cmsPromise;
163
+ }
164
+ ```
165
+
166
+ ### 7. Mount the API
167
+
168
+ Create `src/app/api/[[...path]]/route.ts`:
169
+
170
+ ```ts
171
+ import { NextResponse } from 'next/server';
172
+ import { getServerSession } from 'next-auth';
173
+ import { createCmsApiHandler } from '@infuro/cms-core/api';
174
+ import { CMS_ENTITY_MAP } from '@infuro/cms-core';
175
+ import { getDataSourceInitialized } from '@/lib/data-source';
176
+ import {
177
+ requireAuth,
178
+ requireAdminAccess,
179
+ requireEntityPermission,
180
+ getAuthenticatedUser,
181
+ } from '@/lib/auth-helpers';
182
+ import { getCms } from '@/lib/cms';
183
+ import bcrypt from 'bcryptjs';
184
+
185
+ const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
186
+
187
+ async function requireAdminApiAuth(req: Request) {
188
+ const a = await requireAuth(req);
189
+ if (a) return a;
190
+ return requireAdminAccess(req);
191
+ }
192
+
193
+ let handlerPromise: Promise<ReturnType<typeof createCmsApiHandler>> | null = null;
194
+
195
+ async function getHandler() {
196
+ if (!handlerPromise) {
197
+ const dataSource = await getDataSourceInitialized();
198
+ handlerPromise = Promise.resolve(
199
+ createCmsApiHandler({
200
+ dataSource,
201
+ entityMap: CMS_ENTITY_MAP,
202
+ requireAuth: requireAdminApiAuth,
203
+ requireEntityPermission,
204
+ getSessionUser: getAuthenticatedUser,
205
+ json: NextResponse.json.bind(NextResponse),
206
+ getCms,
207
+ userAuth: {
208
+ dataSource,
209
+ entityMap: CMS_ENTITY_MAP,
210
+ json: NextResponse.json.bind(NextResponse),
211
+ baseUrl,
212
+ hashPassword: (p) => Promise.resolve(bcrypt.hashSync(p, 12)),
213
+ comparePassword: (p, h) => Promise.resolve(bcrypt.compareSync(p, h)),
214
+ resetExpiryHours: 1,
215
+ getSession: () =>
216
+ getServerSession().then((s) => (s ? { user: s.user } : null)),
217
+ },
218
+ dashboard: {
219
+ dataSource,
220
+ entityMap: CMS_ENTITY_MAP,
221
+ json: NextResponse.json.bind(NextResponse),
222
+ requireAuth: requireAdminApiAuth,
223
+ requirePermission: requireAdminApiAuth,
224
+ },
225
+ upload: {
226
+ json: NextResponse.json.bind(NextResponse),
227
+ requireAuth: requireAdminApiAuth,
228
+ storage: () => getCms().then((cms) => cms.getPlugin('storage')),
229
+ localUploadDir: 'public/uploads',
230
+ },
231
+ blogBySlug: {
232
+ dataSource,
233
+ entityMap: CMS_ENTITY_MAP,
234
+ json: NextResponse.json.bind(NextResponse),
235
+ requireAuth: async () => null,
236
+ },
237
+ formBySlug: {
238
+ dataSource,
239
+ entityMap: CMS_ENTITY_MAP,
240
+ json: NextResponse.json.bind(NextResponse),
241
+ requireAuth: async () => null,
242
+ },
243
+ usersApi: {
244
+ dataSource,
245
+ entityMap: CMS_ENTITY_MAP,
246
+ json: NextResponse.json.bind(NextResponse),
247
+ requireAuth: requireAdminApiAuth,
248
+ baseUrl,
249
+ },
250
+ })
251
+ );
252
+ }
253
+ return handlerPromise;
254
+ }
255
+
256
+ async function handle(method: string, req: Request, context: { params: Promise<{ path?: string[] }> }) {
257
+ try {
258
+ const handler = await getHandler();
259
+ const { path = [] } = await context.params;
260
+ return handler.handle(method, path, req);
261
+ } catch {
262
+ return NextResponse.json({ error: 'Server Error' }, { status: 500 });
263
+ }
264
+ }
265
+
266
+ export async function GET(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('GET', req, ctx); }
267
+ export async function POST(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('POST', req, ctx); }
268
+ export async function PUT(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('PUT', req, ctx); }
269
+ export async function PATCH(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('PATCH', req, ctx); }
270
+ export async function DELETE(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('DELETE', req, ctx); }
271
+ ```
272
+
273
+ ### 8. Mount NextAuth
274
+
275
+ Create `src/app/api/auth/[...nextauth]/route.ts`:
276
+
277
+ ```ts
278
+ import NextAuth from 'next-auth';
279
+ import { getNextAuthOptions } from '@infuro/cms-core/auth';
280
+ import { getDataSourceInitialized } from '@/lib/data-source';
281
+ import { CMS_ENTITY_MAP } from '@infuro/cms-core';
282
+ import bcrypt from 'bcryptjs';
283
+
284
+ async function getOptions() {
285
+ const dataSource = await getDataSourceInitialized();
286
+ const userRepo = dataSource.getRepository(CMS_ENTITY_MAP.users);
287
+ return getNextAuthOptions({
288
+ getUserByEmail: async (email: string) => {
289
+ return userRepo.findOne({
290
+ where: { email },
291
+ relations: ['group', 'group.permissions'],
292
+ select: ['id', 'email', 'name', 'password', 'blocked', 'deleted', 'groupId'],
293
+ }) as any;
294
+ },
295
+ comparePassword: (plain, hash) => Promise.resolve(bcrypt.compareSync(plain, hash)),
296
+ signInPage: '/admin/signin',
297
+ });
298
+ }
299
+
300
+ let handler: ReturnType<typeof NextAuth> | null = null;
301
+
302
+ async function getHandler() {
303
+ if (!handler) handler = NextAuth(await getOptions());
304
+ return handler;
305
+ }
306
+
307
+ export async function GET(req: Request) {
308
+ return ((await getHandler()) as any).GET(req);
309
+ }
310
+ export async function POST(req: Request) {
311
+ return ((await getHandler()) as any).POST(req);
312
+ }
313
+ ```
314
+
315
+ ### 9. Mount the admin panel
316
+
317
+ Create `src/app/admin/layout.tsx`:
318
+
319
+ ```tsx
320
+ 'use client';
321
+ import '@infuro/cms-core/admin.css';
322
+ import AdminLayout from '@infuro/cms-core/admin';
323
+
324
+ export default function AdminLayoutWrapper({ children }: { children: React.ReactNode }) {
325
+ return (
326
+ <AdminLayout
327
+ customNavItems={[]}
328
+ customNavSections={[]}
329
+ customCrudConfigs={{}}
330
+ >
331
+ {children}
332
+ </AdminLayout>
333
+ );
334
+ }
335
+ ```
336
+
337
+ Create `src/app/admin/[[...slug]]/page.tsx`:
338
+
339
+ ```tsx
340
+ import { AdminPageResolver } from '@infuro/cms-core/admin';
341
+
342
+ export default async function AdminPage({ params }: { params: Promise<{ slug?: string[] }> }) {
343
+ const { slug } = await params;
344
+ return <AdminPageResolver slug={slug} />;
345
+ }
346
+ ```
347
+
348
+ The admin at `/admin` is rendered by the package (layout, sidebar, header, built-in pages). Pass `customNavSections` and `customCrudConfigs` to add your own sidebar links and CRUD list pages (see [Adding custom pages and admin nav](#adding-custom-pages-and-admin-nav)).
349
+
350
+ ### 10. Configure Tailwind
351
+
352
+ Core's admin components use Tailwind classes. Include the package in `content` so those classes aren't purged:
353
+
354
+ ```js
355
+ content: [
356
+ "./src/**/*.{js,ts,jsx,tsx,mdx}",
357
+ // When using from npm:
358
+ "./node_modules/@infuro/cms-core/dist/**/*.{js,cjs}",
359
+ // When using file:../core (local):
360
+ // "../core/src/**/*.{js,ts,jsx,tsx}",
361
+ ],
362
+ ```
363
+
364
+ You also need the shadcn/ui color mappings in `theme.extend.colors` — see the [Tailwind Config](#tailwind-config) section below.
365
+
366
+ ### 11. Add middleware
367
+
368
+ Create `src/middleware.ts`:
369
+
370
+ ```ts
371
+ import { NextResponse } from 'next/server';
372
+ import type { NextRequest } from 'next/server';
373
+ import { createCmsMiddleware } from '@infuro/cms-core/auth';
374
+
375
+ const cmsMiddleware = createCmsMiddleware({
376
+ // Optional: allow unauthenticated access to specific API paths/methods (e.g. public form submit)
377
+ publicApiMethods: {
378
+ '/api/contacts': ['POST'],
379
+ '/api/form-submissions': ['POST'],
380
+ '/api/blogs': ['GET'],
381
+ '/api/forms': ['GET'],
382
+ '/api/auth': ['GET', 'POST'],
383
+ '/api/users/forgot-password': ['POST'],
384
+ '/api/users/set-password': ['POST'],
385
+ '/api/users/invite': ['POST'],
386
+ },
387
+ });
388
+
389
+ export function middleware(request: NextRequest) {
390
+ const result = cmsMiddleware({
391
+ nextUrl: request.nextUrl,
392
+ url: request.url,
393
+ method: request.method,
394
+ cookies: request.cookies,
395
+ });
396
+
397
+ if (result.type === 'next') return NextResponse.next();
398
+ if (result.type === 'redirect') return NextResponse.redirect(result.url);
399
+ if (result.type === 'json') return NextResponse.json(result.body, { status: result.status });
400
+ return NextResponse.next();
401
+ }
402
+
403
+ export const config = {
404
+ matcher: ['/admin/:path*', '/api/:path*'],
405
+ };
406
+ ```
407
+
408
+ ### 12. Add providers
409
+
410
+ Wrap your root layout with session and theme providers:
411
+
412
+ ```tsx
413
+ // src/app/providers.tsx
414
+ "use client";
415
+ import { ThemeProvider } from "next-themes";
416
+ import { SessionProvider } from "next-auth/react";
417
+ import { Toaster } from "sonner";
418
+
419
+ export function Providers({ children }: { children: React.ReactNode }) {
420
+ return (
421
+ <SessionProvider>
422
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
423
+ {children}
424
+ <Toaster position="top-right" />
425
+ </ThemeProvider>
426
+ </SessionProvider>
427
+ );
428
+ }
429
+ ```
430
+
431
+ Use it in `src/app/layout.tsx`:
432
+
433
+ ```tsx
434
+ import { Providers } from './providers';
435
+
436
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
437
+ return (
438
+ <html lang="en">
439
+ <body>
440
+ <Providers>{children}</Providers>
441
+ </body>
442
+ </html>
443
+ );
444
+ }
445
+ ```
446
+
447
+ ## Adding custom pages and admin nav
448
+
449
+ **Custom sidebar links:** Pass `customNavItems` or `customNavSections` to `AdminLayout` in your `src/app/admin/layout.tsx`. Each item has `href`, `label`, and optional `icon`. Use `href` like `/admin/locations` so the link opens under the admin.
450
+
451
+ **Custom CRUD (list + optional add/edit):** Define a `CustomCrudConfig` (title, apiEndpoint, columns, addEditPageUrl, optional filters) and pass it as `customCrudConfigs={{ myResource: config }}` to `AdminLayout`. You must have a corresponding API (e.g. under your catch-all or a custom route) and entity. The first path segment (e.g. `locations`) is the key; add a nav item with `href: '/admin/locations'`.
452
+
453
+ **Custom full pages:** For a page that isn’t a CRUD list, add a Next.js route under admin, e.g. `src/app/admin/reports/page.tsx`, and render your component there. The admin layout wraps all `/admin/*` routes, so your page appears inside the same shell. Add a link in `customNavItems` or `customNavSections` with `href: '/admin/reports'`.
454
+
455
+ Types are exported from `@infuro/cms-core/admin`: `CustomNavItem`, `CustomNavSection`, `CustomCrudConfig`, `CustomCrudColumn`, etc.
456
+
457
+ ## Database Setup
458
+
459
+ ### First-time setup (quick)
460
+
461
+ For initial development, temporarily set `synchronize: true` in your `data-source.ts`, then run the seed script:
462
+
463
+ ```bash
464
+ npx tsx src/lib/seed.ts
465
+ ```
466
+
467
+ This creates all tables and inserts default data (admin user, categories, tags, forms). Switch `synchronize` back to `false` afterwards.
468
+
469
+ ### Migrations (production)
470
+
471
+ TypeORM CLI requires `tsx` and `dotenv/config` to load TypeScript data sources with `.env` support. The `TYPEORM_CLI=1` env var enables the migrations path (kept off at runtime to avoid Next.js loading `.ts` migration files):
472
+
473
+ ```bash
474
+ # Generate a migration from entity changes
475
+ TYPEORM_CLI=1 npx tsx -r dotenv/config node_modules/typeorm/cli.js migration:generate -d src/lib/data-source.ts src/migrations/MyMigration
476
+
477
+ # Run pending migrations
478
+ TYPEORM_CLI=1 npx tsx -r dotenv/config node_modules/typeorm/cli.js migration:run -d src/lib/data-source.ts
479
+
480
+ # Revert last migration
481
+ TYPEORM_CLI=1 npx tsx -r dotenv/config node_modules/typeorm/cli.js migration:revert -d src/lib/data-source.ts
482
+ ```
483
+
484
+ Your `data-source.ts` should conditionally include migrations and export a default:
485
+
486
+ ```ts
487
+ export function getDataSource(): DataSource {
488
+ if (!dataSource) {
489
+ dataSource = new DataSource({
490
+ type: 'postgres',
491
+ url: process.env.DATABASE_URL,
492
+ entities: Object.values(CMS_ENTITY_MAP),
493
+ synchronize: false,
494
+ ...(process.env.TYPEORM_CLI && { migrations: ['src/migrations/*.ts'] }),
495
+ });
496
+ }
497
+ return dataSource;
498
+ }
499
+
500
+ export default getDataSource();
501
+ ```
502
+
503
+ ## Core Entities
504
+
505
+ | Entity | Table | Purpose |
506
+ |--------|-------|---------|
507
+ | `User` | `users` | Admin users with groups/permissions |
508
+ | `UserGroup` | `user_groups` | Role-based groups |
509
+ | `Permission` | `permissions` | Granular permissions |
510
+ | `Blog` | `blogs` | Blog posts with slug, SEO, tags, categories |
511
+ | `Category` | `categories` | Blog categories |
512
+ | `Tag` | `tags` | Blog tags (many-to-many with blogs) |
513
+ | `Comment` | `comments` | Blog comments |
514
+ | `Contact` | `contacts` | Contact form submissions |
515
+ | `Form` | `forms` | Dynamic forms |
516
+ | `FormField` | `form_fields` | Form field definitions |
517
+ | `FormSubmission` | `form_submissions` | Form submission data |
518
+ | `Seo` | `seos` | SEO metadata |
519
+ | `Config` | `configs` | Key-value configuration |
520
+ | `PasswordResetToken` | `password_reset_tokens` | Password reset flow |
521
+
522
+ ## API Endpoints
523
+
524
+ All mounted under `/api` via the single catch-all route:
525
+
526
+ | Endpoint | Methods | Auth | Description |
527
+ |----------|---------|------|-------------|
528
+ | `/api/{resource}` | GET, POST | Yes | CRUD list/create for any entity in `CMS_ENTITY_MAP` |
529
+ | `/api/{resource}/{id}` | GET, PUT, DELETE | Yes | CRUD get/update/delete by ID |
530
+ | `/api/blogs/slug/{slug}` | GET | No | Public blog by slug |
531
+ | `/api/forms/slug/{slug}` | GET | No | Public form by slug |
532
+ | `/api/users` | GET, POST | Yes | User management |
533
+ | `/api/users/{id}` | GET, PUT, DELETE | Yes | User by ID |
534
+ | `/api/users/forgot-password` | POST | No | Password reset request |
535
+ | `/api/users/set-password` | POST | No | Set new password |
536
+ | `/api/users/invite` | POST | No | Accept invite |
537
+ | `/api/dashboard/stats` | GET | Yes | Dashboard statistics |
538
+ | `/api/analytics` | GET | Yes | Analytics data |
539
+ | `/api/upload` | POST | Yes | File upload |
540
+ | `/api/auth/*` | GET, POST | No | NextAuth routes |
541
+
542
+ ## Plugin System
543
+
544
+ Plugins are initialized via `createCmsApp` and accessed with `cms.getPlugin('name')`.
545
+
546
+ ### Built-in Plugins
547
+
548
+ | Plugin | Factory | Purpose |
549
+ |--------|---------|---------|
550
+ | Storage (S3) | `s3StoragePlugin({...})` | S3 file uploads |
551
+ | Storage (Local) | `localStoragePlugin({dir})` | Local file uploads |
552
+ | Email | `emailPlugin({type, from, ...})` | Email via SMTP/SES/Gmail |
553
+ | Analytics | `analyticsPlugin({...})` | Google Analytics integration |
554
+ | ERP | `erpPlugin({...})` | ERP/CRM integration |
555
+ | SMS | `smsPlugin({...})` | SMS notifications |
556
+ | Payment | `paymentPlugin({...})` | Payment processing |
557
+
558
+ ### Custom Plugins
559
+
560
+ Implement the `CmsPlugin` interface:
561
+
562
+ ```ts
563
+ import type { CmsPlugin, PluginContext } from '@infuro/cms-core';
564
+
565
+ export const myPlugin: CmsPlugin<MyService> = {
566
+ name: 'my-plugin',
567
+ version: '1.0.0',
568
+ async init(context: PluginContext) {
569
+ return new MyService(context.config);
570
+ },
571
+ };
572
+ ```
573
+
574
+ Register it in `cms.ts`:
575
+
576
+ ```ts
577
+ plugins: [
578
+ localStoragePlugin({ dir: 'public/uploads' }),
579
+ myPlugin,
580
+ ],
581
+ ```
582
+
583
+ Access it anywhere:
584
+
585
+ ```ts
586
+ const cms = await getCms();
587
+ const service = cms.getPlugin<MyService>('my-plugin');
588
+ ```
589
+
590
+ ## Package Exports
591
+
592
+ | Import Path | Contents |
593
+ |-------------|----------|
594
+ | `@infuro/cms-core` | Entities, plugins, registry, utilities |
595
+ | `@infuro/cms-core/api` | `createCmsApiHandler`, CRUD handlers, auth handlers |
596
+ | `@infuro/cms-core/auth` | `createAuthHelpers`, `createCmsMiddleware`, `getNextAuthOptions` |
597
+ | `@infuro/cms-core/admin` | Admin layout, pages, components (React, `'use client'`) |
598
+ | `@infuro/cms-core/hooks` | `useIsMobile`, `useAnalytics`, `usePlugin` |
599
+
600
+ ## Extending
601
+
602
+ ### Adding custom entities
603
+
604
+ 1. Define your TypeORM entity
605
+ 2. Add it to a merged entity map:
606
+
607
+ ```ts
608
+ import { CMS_ENTITY_MAP } from '@infuro/cms-core';
609
+ import { Product } from './entities/product.entity';
610
+
611
+ const ENTITY_MAP = { ...CMS_ENTITY_MAP, products: Product };
612
+ ```
613
+
614
+ 3. Pass the merged map to `getDataSource()` entities and `createCmsApiHandler({ entityMap })`
615
+
616
+ ### Adding custom API routes
617
+
618
+ Add files alongside the catch-all (e.g. `src/app/api/my-custom/route.ts`). Next.js resolves specific routes before the catch-all.
619
+
620
+ ### Customizing middleware
621
+
622
+ Pass config to `createCmsMiddleware()`:
623
+
624
+ ```ts
625
+ createCmsMiddleware({
626
+ publicAdminPaths: ['/admin/signin', '/admin/custom-public-page'],
627
+ publicApiMethods: {
628
+ '/api/products': ['GET'],
629
+ },
630
+ });
631
+ ```
632
+
633
+ ## Tailwind Config
634
+
635
+ The admin panel and UI components use shadcn/ui and require CSS variable-based color mappings. Your `tailwind.config.js` needs these in `theme.extend.colors`:
636
+
637
+ ```js
638
+ module.exports = {
639
+ content: [
640
+ "./src/**/*.{js,ts,jsx,tsx,mdx}",
641
+ "./node_modules/@infuro/cms-core/dist/**/*.{js,cjs}",
642
+ ],
643
+ darkMode: "class",
644
+ theme: {
645
+ extend: {
646
+ colors: {
647
+ background: "hsl(var(--background))",
648
+ foreground: "hsl(var(--foreground))",
649
+ card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
650
+ popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))" },
651
+ primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
652
+ secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
653
+ muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
654
+ accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
655
+ destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
656
+ border: "hsl(var(--border))",
657
+ input: "hsl(var(--input))",
658
+ ring: "hsl(var(--ring))",
659
+ sidebar: {
660
+ DEFAULT: "hsl(var(--sidebar-background))",
661
+ foreground: "hsl(var(--sidebar-foreground))",
662
+ primary: "hsl(var(--sidebar-primary))",
663
+ "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
664
+ accent: "hsl(var(--sidebar-accent))",
665
+ "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
666
+ border: "hsl(var(--sidebar-border))",
667
+ ring: "hsl(var(--sidebar-ring))",
668
+ },
669
+ },
670
+ borderRadius: {
671
+ lg: "var(--radius)",
672
+ md: "calc(var(--radius) - 2px)",
673
+ sm: "calc(var(--radius) - 4px)",
674
+ },
675
+ },
676
+ },
677
+ plugins: [require("tailwindcss-animate")],
678
+ };
679
+ ```
680
+
681
+ The CSS variables are injected by the admin layout at runtime. Your website's own CSS can also define them in `:root` if your public pages use shadcn/ui components.
682
+
683
+ ## Environment Variables
684
+
685
+ | Variable | Required | Description |
686
+ |----------|----------|-------------|
687
+ | `DATABASE_URL` | Yes | PostgreSQL connection string |
688
+ | `NEXTAUTH_SECRET` | Yes | NextAuth JWT secret |
689
+ | `NEXTAUTH_URL` | Yes | App base URL |
690
+ | `STORAGE_TYPE` | No | `s3` or `local` (default: local) |
691
+ | `AWS_BUCKET_NAME` | If S3 | S3 bucket name |
692
+ | `AWS_REGION` | If S3/SES | AWS region |
693
+ | `AWS_ACCESS_KEY_ID` | If S3/SES | AWS access key |
694
+ | `AWS_SECRET_ACCESS_KEY` | If S3/SES | AWS secret key |
695
+ | `SMTP_TYPE` | No | `SMTP`, `AWS`, or `GMAIL` |
696
+ | `SMTP_FROM` | If email | Sender email |
697
+ | `SMTP_TO` | If email | Default recipient |
698
+ | `SMTP_USER` | If SMTP | SMTP username |
699
+ | `SMTP_PASSWORD` | If SMTP | SMTP password |
700
+ | `GOOGLE_ANALYTICS_PRIVATE_KEY` | If analytics | GA service account key |
701
+ | `GOOGLE_ANALYTICS_CLIENT_EMAIL` | If analytics | GA service account email |
702
+ | `GOOGLE_ANALYTICS_VIEW_ID` | If analytics | GA property/view ID |
703
+
704
+ ## Development
705
+
706
+ ### Quick start (existing website using core)
707
+
708
+ ```bash
709
+ # 1. Build core (once, or use watch mode)
710
+ cd core
711
+ npm run build
712
+
713
+ # 2. Install website dependencies (links core via file:../core)
714
+ cd ../my-website
715
+ npm install
716
+
717
+ # 3. Set up .env (DATABASE_URL, NEXTAUTH_SECRET, NEXTAUTH_URL)
718
+
719
+ # 4. Create tables & seed (set synchronize: true in data-source.ts first)
720
+ npx tsx src/lib/seed.ts
721
+ # Then set synchronize back to false
722
+
723
+ # 5. Start dev server
724
+ npm run dev
725
+ ```
726
+
727
+ ### Watch mode (developing core + website simultaneously)
728
+
729
+ Terminal 1:
730
+ ```bash
731
+ cd core && npm run dev
732
+ ```
733
+
734
+ Terminal 2:
735
+ ```bash
736
+ cd my-website && npm run dev
737
+ ```
738
+
739
+ Changes to core are picked up automatically by the website's dev server.