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