@infuro/cms-core 1.0.15 → 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.
- package/README.md +739 -724
- package/dist/admin.cjs +1840 -741
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.d.cts +4 -0
- package/dist/admin.d.ts +4 -0
- package/dist/admin.js +1795 -681
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +700 -77
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.js +696 -75
- package/dist/api.js.map +1 -1
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/cli.cjs +21 -6
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +21 -6
- package/dist/cli.js.map +1 -1
- package/dist/hooks.cjs +159 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +24 -1
- package/dist/hooks.d.ts +24 -1
- package/dist/hooks.js +165 -0
- package/dist/hooks.js.map +1 -1
- package/dist/{index-BQnqJ7EO.d.cts → index-D2C1O9b4.d.cts} +22 -3
- package/dist/{index-BiagwMjV.d.ts → index-GMn7-9PX.d.ts} +22 -3
- package/dist/index.cjs +5334 -4336
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -11
- package/dist/index.d.ts +89 -11
- package/dist/index.js +5333 -4352
- package/dist/index.js.map +1 -1
- package/dist/migrations/1772178563554-InitialSchema.ts +304 -304
- package/dist/migrations/1772178563555-ChatAndKnowledgeBase.ts +55 -55
- package/dist/migrations/1772178563556-KnowledgeBaseVector.ts +16 -16
- package/dist/migrations/1774300000000-RbacSeedGroupsAndPermissionUnique.ts +24 -24
- package/dist/migrations/1774300000001-SeedAdministratorUsersPermission.ts +35 -35
- package/dist/migrations/1774400000000-CustomerAdminAccessContactUser.ts +37 -37
- package/dist/migrations/1774400000001-StorefrontCartWishlist.ts +100 -100
- package/dist/migrations/1774400000002-WishlistGuestId.ts +29 -29
- package/dist/migrations/1774500000000-ProductCollectionHsn.ts +15 -15
- package/dist/migrations/1774600000000-OrderKindParentOrderNumber.ts +36 -36
- package/dist/migrations/1774800000000-OtpChallengesUserPhone.ts +41 -41
- package/dist/migrations/1774900000000-MessageTemplates.ts +39 -39
- package/dist/migrations/1775000000000-ProductUomTypeOrderItemSnapshots.ts +29 -29
- package/dist/migrations/1775200000000-MediaDriveFolders.ts +38 -0
- package/dist/migrations/README.md +3 -3
- package/dist/theme.cjs.map +1 -1
- package/dist/theme.js.map +1 -1
- package/package.json +20 -15
- 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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
cmsPromise
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
import {
|
|
172
|
-
import {
|
|
173
|
-
import {
|
|
174
|
-
import {
|
|
175
|
-
import {
|
|
176
|
-
import
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
async
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
async function
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
|
512
|
-
|
|
513
|
-
|
|
|
514
|
-
|
|
|
515
|
-
|
|
|
516
|
-
|
|
|
517
|
-
|
|
|
518
|
-
|
|
|
519
|
-
|
|
|
520
|
-
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
|
534
|
-
|
|
535
|
-
|
|
|
536
|
-
|
|
|
537
|
-
|
|
|
538
|
-
|
|
|
539
|
-
|
|
|
540
|
-
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
},
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
|
686
|
-
|
|
687
|
-
| `
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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.
|