@murumets-ee/create 0.1.12 → 0.1.14
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/dist/cli.mjs +3246 -0
- package/dist/{index.d.ts → index.d.mts} +8 -5
- package/dist/index.d.mts.map +1 -0
- package/dist/{index.js → index.mjs} +1265 -122
- package/dist/index.mjs.map +1 -0
- package/dist/utils-Bmw75lEa.mjs +2 -0
- package/dist/utils-Bmw75lEa.mjs.map +1 -0
- package/package.json +7 -7
- package/dist/chunk-CUB6XFPE.js +0 -2
- package/dist/chunk-ZAX4VH4C.js +0 -3
- package/dist/cli.js +0 -2104
- package/dist/utils-42K2ZGUL.js +0 -1
- package/dist/utils-VX57GKZX.js +0 -2
|
@@ -1,14 +1,1123 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{i as e,n as t,o as n,r,t as i}from"./utils-Bmw75lEa.mjs";import{join as a}from"node:path";import{mkdir as o}from"node:fs/promises";async function s(e,t){await n(a(e,`lib/admin-config.ts`),`import { Media } from '@murumets-ee/media'
|
|
2
|
+
import { Article, Category } from '@/entities'
|
|
3
|
+
|
|
4
|
+
/** All entities available in the admin (drives sidebar nav + CRUD) */
|
|
5
|
+
export const allEntities = [Article, Media, Category]
|
|
6
|
+
|
|
7
|
+
/** Taxonomy entities keyed by name */
|
|
8
|
+
export const taxonomyVocabularies = { category: Category }
|
|
9
|
+
|
|
10
|
+
/** Entities exposed via generic CRUD API handler */
|
|
11
|
+
export const crudEntities = [Article, Media]
|
|
12
|
+
|
|
13
|
+
/** Plugin resources for the permission catalog */
|
|
14
|
+
export const pluginResources = [
|
|
15
|
+
{ resource: 'storage', actions: ['view', 'create', 'update', 'delete'] },
|
|
16
|
+
{ resource: 'settings', actions: ['view', 'update'] },
|
|
17
|
+
{ resource: 'audit-logs', actions: ['view'] },
|
|
18
|
+
{ resource: 'permissions', actions: ['view', 'create', 'update', 'delete'] },
|
|
19
|
+
]
|
|
20
|
+
`),await n(a(e,`lib/content-locale.ts`),`import { cookies } from 'next/headers'
|
|
21
|
+
import { hasLocale } from 'next-intl'
|
|
22
|
+
import { routing } from '@/i18n/routing'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the content editing locale from the cookie, falling back to the interface locale.
|
|
26
|
+
*/
|
|
27
|
+
export async function getContentLocale(interfaceLocale: string): Promise<string> {
|
|
28
|
+
const jar = await cookies()
|
|
29
|
+
const raw = jar.get('content-locale')?.value
|
|
30
|
+
if (raw && hasLocale(routing.locales, raw)) return raw
|
|
31
|
+
return interfaceLocale
|
|
32
|
+
}
|
|
33
|
+
`),await n(a(e,`lib/with-admin-context.ts`),`import { getContentConfig } from '@murumets-ee/content/plugin'
|
|
34
|
+
import type { RequestContext } from '@murumets-ee/core'
|
|
35
|
+
import { runWithContextAsync } from '@murumets-ee/core'
|
|
36
|
+
import { getToolkitApp } from './app'
|
|
37
|
+
import { getContentLocale } from './content-locale'
|
|
38
|
+
|
|
39
|
+
export async function withAdminContext<T>(
|
|
40
|
+
interfaceLocale: string,
|
|
41
|
+
fn: (ctx: { locale: string; defaultLocale: string }) => Promise<T>,
|
|
42
|
+
): Promise<T> {
|
|
43
|
+
const app = await getToolkitApp()
|
|
44
|
+
const contentLocale = await getContentLocale(interfaceLocale)
|
|
45
|
+
const { defaultLocale } = getContentConfig()
|
|
46
|
+
|
|
47
|
+
const context: RequestContext = {
|
|
48
|
+
locale: contentLocale,
|
|
49
|
+
defaultLocale,
|
|
50
|
+
app,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return runWithContextAsync(context, () => fn({ locale: contentLocale, defaultLocale }))
|
|
54
|
+
}
|
|
55
|
+
`),await n(a(e,`lib/load-roles.ts`),`import { buildInitialRoleDefinitions } from '@murumets-ee/auth'
|
|
56
|
+
import type { ToolkitApp } from '@murumets-ee/core'
|
|
57
|
+
import { createSettingsClient } from '@murumets-ee/settings'
|
|
58
|
+
import { permissionSettings } from '@/settings/permissions'
|
|
59
|
+
|
|
60
|
+
export async function loadRoles(
|
|
61
|
+
app: ToolkitApp,
|
|
62
|
+
): Promise<Record<string, Record<string, string[]>>> {
|
|
63
|
+
const client = createSettingsClient(permissionSettings, { app })
|
|
64
|
+
const saved = await client.get('roles')
|
|
65
|
+
|
|
66
|
+
if (saved) return saved
|
|
67
|
+
|
|
68
|
+
// First run — seed built-in roles with zero permissions
|
|
69
|
+
const initial = buildInitialRoleDefinitions()
|
|
70
|
+
await client.set('roles', initial)
|
|
71
|
+
return initial
|
|
72
|
+
}
|
|
73
|
+
`),await n(a(e,`settings/permissions.ts`),`import { defineSettings, setting } from '@murumets-ee/settings'
|
|
74
|
+
|
|
75
|
+
export const permissionSettings = defineSettings({
|
|
76
|
+
namespace: 'permissions',
|
|
77
|
+
scope: 'global',
|
|
78
|
+
label: 'Permissions',
|
|
79
|
+
schema: {
|
|
80
|
+
roles: setting.json<Record<string, Record<string, string[]>>>(),
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
`),await n(a(e,`settings/site.ts`),`import { defineSettings, setting } from '@murumets-ee/settings'
|
|
84
|
+
|
|
85
|
+
export const siteSettings = defineSettings({
|
|
86
|
+
namespace: 'site',
|
|
87
|
+
scope: 'global',
|
|
88
|
+
label: 'Site Settings',
|
|
89
|
+
schema: {
|
|
90
|
+
siteName: setting.text({ default: 'My Site', label: 'Site Name', translatable: true }),
|
|
91
|
+
siteDescription: setting.text({ label: 'Site Description', translatable: true }),
|
|
92
|
+
maintenanceMode: setting.boolean({ default: false, label: 'Maintenance Mode' }),
|
|
93
|
+
postsPerPage: setting.number({
|
|
94
|
+
default: 10,
|
|
95
|
+
min: 1,
|
|
96
|
+
max: 100,
|
|
97
|
+
integer: true,
|
|
98
|
+
label: 'Posts Per Page',
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
`),await n(a(e,`app/admin-layout.tsx`),`'use client'
|
|
103
|
+
|
|
104
|
+
import type { LinkComponent, SidebarNavGroup } from '@murumets-ee/admin-ui'
|
|
105
|
+
import { AdminShell } from '@murumets-ee/admin-ui'
|
|
106
|
+
import type { AdminNavGroup } from '@murumets-ee/admin-ui/server'
|
|
107
|
+
import {
|
|
108
|
+
Activity,
|
|
109
|
+
FileText,
|
|
110
|
+
FolderTree,
|
|
111
|
+
Home,
|
|
112
|
+
Image,
|
|
113
|
+
ImageDown,
|
|
114
|
+
Lock,
|
|
115
|
+
PenTool,
|
|
116
|
+
ShieldCheck,
|
|
117
|
+
Tags,
|
|
118
|
+
Users,
|
|
119
|
+
type LucideIcon,
|
|
120
|
+
} from 'lucide-react'
|
|
121
|
+
import type { ReactNode } from 'react'
|
|
122
|
+
import { Link, usePathname } from '@/i18n/navigation'
|
|
123
|
+
|
|
124
|
+
/** Map from icon name strings (from entity admin config) to Lucide components */
|
|
125
|
+
const ICON_MAP: Record<string, LucideIcon> = {
|
|
126
|
+
'file-text': FileText,
|
|
127
|
+
'folder-tree': FolderTree,
|
|
128
|
+
tags: Tags,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface AdminLayoutProps {
|
|
132
|
+
children: ReactNode
|
|
133
|
+
defaultOpen?: boolean
|
|
134
|
+
headerActions?: ReactNode
|
|
135
|
+
sidebarFooter?: ReactNode
|
|
136
|
+
entityNavGroups?: AdminNavGroup[]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function AdminLayout({
|
|
140
|
+
children,
|
|
141
|
+
defaultOpen,
|
|
142
|
+
headerActions,
|
|
143
|
+
sidebarFooter,
|
|
144
|
+
entityNavGroups,
|
|
145
|
+
}: AdminLayoutProps) {
|
|
146
|
+
const pathname = usePathname()
|
|
147
|
+
|
|
148
|
+
const dynamicGroups: SidebarNavGroup[] = (entityNavGroups ?? []).map((group) => ({
|
|
149
|
+
label: group.label,
|
|
150
|
+
items: group.items.map((item) => ({
|
|
151
|
+
label: item.label,
|
|
152
|
+
href: item.href,
|
|
153
|
+
icon: item.iconName ? ICON_MAP[item.iconName] : undefined,
|
|
154
|
+
})),
|
|
155
|
+
}))
|
|
156
|
+
|
|
157
|
+
const staticBefore: SidebarNavGroup = {
|
|
158
|
+
items: [
|
|
159
|
+
{ label: 'Home', href: '/', icon: Home },
|
|
160
|
+
{ label: 'Admin', href: '/admin', icon: PenTool },
|
|
161
|
+
],
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const systemGroup: SidebarNavGroup = {
|
|
165
|
+
label: 'System',
|
|
166
|
+
items: [
|
|
167
|
+
{ label: 'Users', href: '/admin/users', icon: Users },
|
|
168
|
+
{ label: 'Roles', href: '/admin/roles', icon: ShieldCheck },
|
|
169
|
+
{ label: 'Permissions', href: '/admin/permissions', icon: Lock },
|
|
170
|
+
{ label: 'Media', href: '/admin/media', icon: Image },
|
|
171
|
+
{ label: 'Image Styles', href: '/admin/media/image-styles', icon: ImageDown },
|
|
172
|
+
{ label: 'Activity', href: '/admin/activity', icon: Activity },
|
|
173
|
+
],
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const navGroups: SidebarNavGroup[] = [
|
|
177
|
+
staticBefore,
|
|
178
|
+
...dynamicGroups,
|
|
179
|
+
systemGroup,
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<AdminShell
|
|
184
|
+
defaultOpen={defaultOpen}
|
|
185
|
+
sidebar={{
|
|
186
|
+
navGroups,
|
|
187
|
+
pathname,
|
|
188
|
+
Link: Link as unknown as LinkComponent,
|
|
189
|
+
logo: (
|
|
190
|
+
<div className="flex h-8 items-center gap-2 px-2">
|
|
191
|
+
<span className="font-semibold text-sm text-zinc-700 dark:text-zinc-300 truncate">
|
|
192
|
+
Admin
|
|
193
|
+
</span>
|
|
194
|
+
</div>
|
|
195
|
+
),
|
|
196
|
+
footer: sidebarFooter,
|
|
197
|
+
}}
|
|
198
|
+
header={{
|
|
199
|
+
actions: headerActions,
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{children}
|
|
203
|
+
</AdminShell>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
`),await n(a(e,`app/[locale]/(shell)/layout.tsx`),`import { buildAdminNav } from '@murumets-ee/admin-ui/server'
|
|
207
|
+
import { cookies, headers } from 'next/headers'
|
|
208
|
+
import { redirect } from 'next/navigation'
|
|
209
|
+
import { Suspense } from 'react'
|
|
210
|
+
import { allEntities } from '@/lib/admin-config'
|
|
211
|
+
import { auth } from '@/lib/auth'
|
|
212
|
+
import { AdminLayout } from '../../admin-layout'
|
|
213
|
+
import { ThemeToggle } from '../../theme-toggle'
|
|
214
|
+
|
|
215
|
+
async function DynamicShell({
|
|
216
|
+
children,
|
|
217
|
+
interfaceLocale,
|
|
218
|
+
}: {
|
|
219
|
+
children: React.ReactNode
|
|
220
|
+
interfaceLocale: string
|
|
221
|
+
}) {
|
|
222
|
+
const h = await headers()
|
|
223
|
+
const session = await auth.api.getSession({ headers: h })
|
|
224
|
+
if (!session?.user) {
|
|
225
|
+
redirect(\`/\${interfaceLocale}/auth/sign-in\`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const jar = await cookies()
|
|
229
|
+
const sidebarOpen = jar.get('sidebar:state')?.value !== 'false'
|
|
230
|
+
const entityNavGroups = buildAdminNav(allEntities, { basePath: '/admin' })
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<AdminLayout
|
|
234
|
+
defaultOpen={sidebarOpen}
|
|
235
|
+
entityNavGroups={entityNavGroups}
|
|
236
|
+
sidebarFooter={
|
|
237
|
+
<div className="flex justify-center gap-2">
|
|
238
|
+
<ThemeToggle />
|
|
239
|
+
</div>
|
|
240
|
+
}
|
|
241
|
+
>
|
|
242
|
+
{children}
|
|
243
|
+
</AdminLayout>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export default async function ShellLayout({
|
|
248
|
+
children,
|
|
249
|
+
params,
|
|
250
|
+
}: {
|
|
251
|
+
children: React.ReactNode
|
|
252
|
+
params: Promise<{ locale: string }>
|
|
253
|
+
}) {
|
|
254
|
+
const { locale } = await params
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<Suspense>
|
|
258
|
+
<DynamicShell interfaceLocale={locale}>{children}</DynamicShell>
|
|
259
|
+
</Suspense>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/layout.tsx`),`import { headers } from 'next/headers'
|
|
263
|
+
import { redirect } from 'next/navigation'
|
|
264
|
+
import { auth } from '@/lib/auth'
|
|
265
|
+
|
|
266
|
+
const ALLOWED_ROLES = new Set(['admin', 'editor'])
|
|
267
|
+
|
|
268
|
+
export default async function AdminGuardLayout({
|
|
269
|
+
children,
|
|
270
|
+
params,
|
|
271
|
+
}: {
|
|
272
|
+
children: React.ReactNode
|
|
273
|
+
params: Promise<{ locale: string }>
|
|
274
|
+
}) {
|
|
275
|
+
const { locale } = await params
|
|
276
|
+
const h = await headers()
|
|
277
|
+
const session = await auth.api.getSession({ headers: h })
|
|
278
|
+
|
|
279
|
+
if (!session?.user) {
|
|
280
|
+
redirect(\`/\${locale}/auth/sign-in\`)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const role = (session.user as Record<string, unknown>).role as string | undefined
|
|
284
|
+
if (!role || !ALLOWED_ROLES.has(role)) {
|
|
285
|
+
redirect(\`/\${locale}\`)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return <>{children}</>
|
|
289
|
+
}
|
|
290
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/page.tsx`),`import { setRequestLocale } from 'next-intl/server'
|
|
291
|
+
|
|
292
|
+
export default async function AdminDashboard({ params }: { params: Promise<{ locale: string }> }) {
|
|
293
|
+
const { locale } = await params
|
|
294
|
+
setRequestLocale(locale)
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div className="p-6">
|
|
298
|
+
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
|
|
299
|
+
<p className="text-muted-foreground">
|
|
300
|
+
Welcome to the admin panel. Use the sidebar to manage your content.
|
|
301
|
+
</p>
|
|
302
|
+
</div>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/[entity]/page.tsx`),`import { EntityList } from '@murumets-ee/admin-ui/entity-list'
|
|
306
|
+
import {
|
|
307
|
+
entityNameToSlug,
|
|
308
|
+
fetchEntityList,
|
|
309
|
+
getEntityLabel,
|
|
310
|
+
resolveEntityFromSlug,
|
|
311
|
+
toEntityMeta,
|
|
312
|
+
} from '@murumets-ee/admin-ui/server'
|
|
313
|
+
import { getContentConfig } from '@murumets-ee/content/plugin'
|
|
314
|
+
import { notFound } from 'next/navigation'
|
|
315
|
+
import { setRequestLocale } from 'next-intl/server'
|
|
316
|
+
import { allEntities, taxonomyVocabularies } from '@/lib/admin-config'
|
|
317
|
+
import { withAdminContext } from '@/lib/with-admin-context'
|
|
318
|
+
|
|
319
|
+
interface EntityListPageProps {
|
|
320
|
+
params: Promise<{ locale: string; entity: string }>
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export default async function EntityListPage({ params }: EntityListPageProps) {
|
|
324
|
+
const { locale, entity: entitySlug } = await params
|
|
325
|
+
setRequestLocale(locale)
|
|
326
|
+
|
|
327
|
+
const entity = resolveEntityFromSlug(entitySlug, allEntities)
|
|
328
|
+
if (!entity) notFound()
|
|
329
|
+
|
|
330
|
+
const admin = entity.admin
|
|
331
|
+
const meta = toEntityMeta(entity)
|
|
332
|
+
const urlSlug = entityNameToSlug(entity.name)
|
|
333
|
+
const hasTranslatable = Object.values(entity.allFields).some((f) => f.translatable)
|
|
334
|
+
const isPublishable = entity.behaviors?.some((b) => b.name === 'publishable')
|
|
335
|
+
const isTaxonomy = entity.name in taxonomyVocabularies
|
|
336
|
+
|
|
337
|
+
return withAdminContext(locale, async ({ locale: contentLocale, defaultLocale }) => {
|
|
338
|
+
let locales: Array<{ code: string; label: string }> | undefined
|
|
339
|
+
if (hasTranslatable) {
|
|
340
|
+
try {
|
|
341
|
+
const config = getContentConfig()
|
|
342
|
+
locales = config.locales
|
|
343
|
+
} catch {
|
|
344
|
+
// content plugin not available
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const initialData = await fetchEntityList(entity, {
|
|
349
|
+
sortField: admin?.defaultSort ?? 'createdAt',
|
|
350
|
+
sortDirection: admin?.defaultSortDirection ?? 'desc',
|
|
351
|
+
limit: admin?.pageSize ?? 20,
|
|
352
|
+
locale: contentLocale,
|
|
353
|
+
includeTranslationStatus: hasTranslatable,
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<div className="p-6">
|
|
358
|
+
<div className="mb-6">
|
|
359
|
+
<h1 className="text-2xl font-bold">{getEntityLabel(entity)}</h1>
|
|
360
|
+
{admin?.description && (
|
|
361
|
+
<p className="text-sm text-muted-foreground">{admin.description}</p>
|
|
362
|
+
)}
|
|
363
|
+
</div>
|
|
364
|
+
<EntityList
|
|
365
|
+
entity={meta}
|
|
366
|
+
allowDelete
|
|
367
|
+
allowStatusToggle={isPublishable}
|
|
368
|
+
{...(isTaxonomy ? { apiBasePath: '/api/admin/taxonomy', entityPath: entity.name } : {})}
|
|
369
|
+
defaultSort={admin?.defaultSort ?? 'createdAt'}
|
|
370
|
+
defaultSortDirection={admin?.defaultSortDirection ?? 'desc'}
|
|
371
|
+
hiddenColumns={admin?.hiddenColumns}
|
|
372
|
+
pageSize={admin?.pageSize}
|
|
373
|
+
locale={contentLocale}
|
|
374
|
+
locales={locales}
|
|
375
|
+
defaultLocale={defaultLocale}
|
|
376
|
+
showTranslationStatus={hasTranslatable && !!locales}
|
|
377
|
+
searchPlaceholder={\`Search \${getEntityLabel(entity).toLowerCase()}...\`}
|
|
378
|
+
editHref={\`/\${locale}/admin/\${urlSlug}/:id\`}
|
|
379
|
+
createHref={admin?.disableCreate ? undefined : \`/\${locale}/admin/\${urlSlug}/new\`}
|
|
380
|
+
initialData={initialData}
|
|
381
|
+
/>
|
|
382
|
+
</div>
|
|
383
|
+
)
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/[entity]/[id]/page.tsx`),`import {
|
|
387
|
+
entityNameToSlug,
|
|
388
|
+
fetchReferenceOptions,
|
|
389
|
+
getBlocksFieldName,
|
|
390
|
+
getEntityLabelSingular,
|
|
391
|
+
getReferenceFields,
|
|
392
|
+
inferRootFields,
|
|
393
|
+
resolveEntityFromSlug,
|
|
394
|
+
toEntityMeta,
|
|
395
|
+
} from '@murumets-ee/admin-ui/server'
|
|
396
|
+
import { buildPermissionChecker } from '@murumets-ee/auth'
|
|
397
|
+
import { ContentClient } from '@murumets-ee/content/client'
|
|
398
|
+
import { LockService } from '@murumets-ee/content/lock'
|
|
399
|
+
import { createAdminClient } from '@murumets-ee/core/clients'
|
|
400
|
+
import { getApp } from '@murumets-ee/core'
|
|
401
|
+
import { prepareBlockEditor } from '@murumets-ee/editor/server'
|
|
402
|
+
import { headers } from 'next/headers'
|
|
403
|
+
import { notFound } from 'next/navigation'
|
|
404
|
+
import { setRequestLocale } from 'next-intl/server'
|
|
405
|
+
import { GenericBlockEditor } from '@murumets-ee/admin-ui/content-editor'
|
|
406
|
+
import { GenericEntityForm } from '@murumets-ee/admin-ui/entity-form'
|
|
407
|
+
import { allEntities } from '@/lib/admin-config'
|
|
408
|
+
import { auth } from '@/lib/auth'
|
|
409
|
+
import { loadRoles } from '@/lib/load-roles'
|
|
410
|
+
import { withAdminContext } from '@/lib/with-admin-context'
|
|
411
|
+
|
|
412
|
+
interface EditEntityPageProps {
|
|
413
|
+
params: Promise<{ locale: string; entity: string; id: string }>
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export default async function EditEntityPage({ params }: EditEntityPageProps) {
|
|
417
|
+
const { locale, entity: entitySlug, id } = await params
|
|
418
|
+
setRequestLocale(locale)
|
|
419
|
+
|
|
420
|
+
const entity = resolveEntityFromSlug(entitySlug, allEntities)
|
|
421
|
+
if (!entity) notFound()
|
|
422
|
+
|
|
423
|
+
const blocksField = getBlocksFieldName(entity)
|
|
424
|
+
const urlSlug = entityNameToSlug(entity.name)
|
|
425
|
+
|
|
426
|
+
return withAdminContext(locale, async ({ locale: contentLocale, defaultLocale }) => {
|
|
427
|
+
const client = createAdminClient(entity)
|
|
428
|
+
const isDefaultLocale = contentLocale === defaultLocale
|
|
429
|
+
const isNonDefaultLocale = !isDefaultLocale
|
|
430
|
+
|
|
431
|
+
const isVersionable = entity.behaviors?.some((b) => b.name === 'versionable') ?? false
|
|
432
|
+
const isPublishable = entity.behaviors?.some((b) => b.name === 'publishable') ?? false
|
|
433
|
+
|
|
434
|
+
let draft: { data: Record<string, unknown>; createdBy: string; createdByName: string | null; updatedAt: Date | string } | undefined
|
|
435
|
+
let canPublish = false
|
|
436
|
+
let lockResult: { acquired: boolean; lock?: { lockedBy: string; lockedByName: string | null; lockedAt: Date | string; expiresAt: Date | string } | null } | undefined
|
|
437
|
+
const enableLocking = isPublishable
|
|
438
|
+
|
|
439
|
+
if (isPublishable) {
|
|
440
|
+
try {
|
|
441
|
+
const app = getApp()
|
|
442
|
+
const contentClient = new ContentClient({ entity, db: app.db.readWrite })
|
|
443
|
+
const lockLocale = isDefaultLocale ? '_' : contentLocale
|
|
444
|
+
|
|
445
|
+
const draftEntry = await contentClient.getDraft(id, lockLocale)
|
|
446
|
+
if (draftEntry) {
|
|
447
|
+
draft = {
|
|
448
|
+
data: draftEntry.data,
|
|
449
|
+
createdBy: draftEntry.createdBy,
|
|
450
|
+
createdByName: draftEntry.createdByName,
|
|
451
|
+
updatedAt: draftEntry.updatedAt,
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const session = await auth.api.getSession({ headers: await headers() })
|
|
456
|
+
if (session?.user) {
|
|
457
|
+
const role = (session.user as Record<string, unknown>).role as string | undefined
|
|
458
|
+
if (role) {
|
|
459
|
+
const roles = await loadRoles(app)
|
|
460
|
+
const checker = buildPermissionChecker(roles)
|
|
461
|
+
canPublish = checker(role, entity.name, 'publish')
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const lockService = new LockService({ db: app.db.readWrite })
|
|
465
|
+
const result = await lockService.acquireLock(
|
|
466
|
+
entity.name, id, lockLocale,
|
|
467
|
+
{ id: session.user.id, name: session.user.name ?? undefined },
|
|
468
|
+
)
|
|
469
|
+
if (result.acquired) {
|
|
470
|
+
lockResult = { acquired: true }
|
|
471
|
+
} else {
|
|
472
|
+
lockResult = { acquired: false, lock: result.lock }
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} catch {
|
|
476
|
+
// Draft/permission/lock errors — proceed without
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Block editor entity
|
|
481
|
+
if (blocksField) {
|
|
482
|
+
const record = isDefaultLocale
|
|
483
|
+
? await client.findById(id, { defaultLocale })
|
|
484
|
+
: await client.findById(id, { locale: contentLocale, defaultLocale })
|
|
485
|
+
if (!record) notFound()
|
|
486
|
+
|
|
487
|
+
const rootFields = inferRootFields(entity, blocksField)
|
|
488
|
+
const { blocks, initialData, mediaUrls, rootData, rootFieldDefs } =
|
|
489
|
+
await prepareBlockEditor(
|
|
490
|
+
entity as Parameters<typeof prepareBlockEditor>[0],
|
|
491
|
+
blocksField,
|
|
492
|
+
record,
|
|
493
|
+
{ rootFields },
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
return (
|
|
497
|
+
<div className="flex h-[calc(100vh-3.5rem)] flex-col">
|
|
498
|
+
<div className="flex items-center gap-3 border-b px-6 py-3">
|
|
499
|
+
<h1 className="text-lg font-semibold">
|
|
500
|
+
{(record.title as string) ?? \`Untitled \${getEntityLabelSingular(entity)}\`}
|
|
501
|
+
</h1>
|
|
502
|
+
<span className="rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground uppercase">
|
|
503
|
+
{contentLocale}
|
|
504
|
+
</span>
|
|
505
|
+
</div>
|
|
506
|
+
<div className="flex-1 overflow-hidden">
|
|
507
|
+
<GenericBlockEditor
|
|
508
|
+
key={contentLocale}
|
|
509
|
+
entityId={id}
|
|
510
|
+
entityName={entity.name}
|
|
511
|
+
blocksField={blocksField}
|
|
512
|
+
blocks={blocks}
|
|
513
|
+
initialData={initialData}
|
|
514
|
+
mediaUrls={mediaUrls}
|
|
515
|
+
rootData={rootData}
|
|
516
|
+
rootFieldDefs={rootFieldDefs}
|
|
517
|
+
saveLabel="Save"
|
|
518
|
+
locale={contentLocale}
|
|
519
|
+
defaultLocale={defaultLocale}
|
|
520
|
+
versionable={isVersionable}
|
|
521
|
+
isPublishable={isPublishable}
|
|
522
|
+
draft={draft}
|
|
523
|
+
canPublish={canPublish}
|
|
524
|
+
enableLocking={enableLocking}
|
|
525
|
+
lockResult={lockResult}
|
|
526
|
+
/>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Form entity
|
|
533
|
+
const [record, defaultLocaleData] = await Promise.all([
|
|
534
|
+
client.findById(id, { locale: contentLocale }),
|
|
535
|
+
isNonDefaultLocale ? client.findById(id) : Promise.resolve(null),
|
|
536
|
+
])
|
|
537
|
+
if (!record) notFound()
|
|
538
|
+
|
|
539
|
+
const refFields = getReferenceFields(entity)
|
|
540
|
+
const referenceOptions: Record<string, Array<{ id: string; label: string }>> = {}
|
|
541
|
+
|
|
542
|
+
if (refFields.length > 0) {
|
|
543
|
+
const optionPromises = refFields.map(async ({ fieldName, entityName }) => {
|
|
544
|
+
const refEntity = allEntities.find((e) => e.name === entityName)
|
|
545
|
+
if (!refEntity) return { fieldName, options: [] }
|
|
546
|
+
const options = await fetchReferenceOptions(refEntity, { locale: contentLocale })
|
|
547
|
+
return { fieldName, options }
|
|
548
|
+
})
|
|
549
|
+
const results = await Promise.all(optionPromises)
|
|
550
|
+
for (const { fieldName, options } of results) {
|
|
551
|
+
referenceOptions[fieldName] = options
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return (
|
|
556
|
+
<div className="p-6">
|
|
557
|
+
<div className="mb-6">
|
|
558
|
+
<h1 className="text-2xl font-bold">Edit {getEntityLabelSingular(entity)}</h1>
|
|
559
|
+
</div>
|
|
560
|
+
<GenericEntityForm
|
|
561
|
+
entity={toEntityMeta(entity)}
|
|
562
|
+
id={id}
|
|
563
|
+
locale={contentLocale}
|
|
564
|
+
defaultLocale={defaultLocale}
|
|
565
|
+
defaultLocaleData={
|
|
566
|
+
isNonDefaultLocale && defaultLocaleData
|
|
567
|
+
? (defaultLocaleData as Record<string, unknown>)
|
|
568
|
+
: undefined
|
|
569
|
+
}
|
|
570
|
+
initialData={record as Record<string, unknown>}
|
|
571
|
+
referenceOptions={referenceOptions}
|
|
572
|
+
listUrl={\`/\${locale}/admin/\${urlSlug}\`}
|
|
573
|
+
versionable={isVersionable}
|
|
574
|
+
draft={draft}
|
|
575
|
+
canPublish={canPublish}
|
|
576
|
+
enableLocking={enableLocking}
|
|
577
|
+
lockResult={lockResult}
|
|
578
|
+
/>
|
|
579
|
+
</div>
|
|
580
|
+
)
|
|
581
|
+
})
|
|
582
|
+
}
|
|
583
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/[entity]/new/page.tsx`),`import {
|
|
584
|
+
entityNameToSlug,
|
|
585
|
+
fetchReferenceOptions,
|
|
586
|
+
getBlocksFieldName,
|
|
587
|
+
getEntityLabelSingular,
|
|
588
|
+
getReferenceFields,
|
|
589
|
+
resolveEntityFromSlug,
|
|
590
|
+
toEntityMeta,
|
|
591
|
+
} from '@murumets-ee/admin-ui/server'
|
|
592
|
+
import { createAdminClient } from '@murumets-ee/core/clients'
|
|
593
|
+
import { notFound, redirect } from 'next/navigation'
|
|
594
|
+
import { setRequestLocale } from 'next-intl/server'
|
|
595
|
+
import { GenericEntityForm } from '@murumets-ee/admin-ui/entity-form'
|
|
596
|
+
import { allEntities } from '@/lib/admin-config'
|
|
597
|
+
import { withAdminContext } from '@/lib/with-admin-context'
|
|
598
|
+
|
|
599
|
+
interface NewEntityPageProps {
|
|
600
|
+
params: Promise<{ locale: string; entity: string }>
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export default async function NewEntityPage({ params }: NewEntityPageProps) {
|
|
604
|
+
const { locale, entity: entitySlug } = await params
|
|
605
|
+
setRequestLocale(locale)
|
|
606
|
+
|
|
607
|
+
const entity = resolveEntityFromSlug(entitySlug, allEntities)
|
|
608
|
+
if (!entity) notFound()
|
|
609
|
+
if (entity.admin?.disableCreate) notFound()
|
|
610
|
+
|
|
611
|
+
const blocksField = getBlocksFieldName(entity)
|
|
612
|
+
const urlSlug = entityNameToSlug(entity.name)
|
|
613
|
+
|
|
614
|
+
return withAdminContext(locale, async ({ locale: contentLocale }) => {
|
|
615
|
+
// Block editor entities: create a draft and redirect to the edit page
|
|
616
|
+
if (blocksField) {
|
|
617
|
+
const client = createAdminClient(entity)
|
|
618
|
+
const draft = await client.create({ title: 'Untitled' })
|
|
619
|
+
const newId = (draft as Record<string, unknown>).id as string
|
|
620
|
+
redirect(\`/\${locale}/admin/\${urlSlug}/\${newId}\`)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Form entities: render the create form
|
|
624
|
+
const refFields = getReferenceFields(entity)
|
|
625
|
+
const referenceOptions: Record<string, Array<{ id: string; label: string }>> = {}
|
|
626
|
+
|
|
627
|
+
if (refFields.length > 0) {
|
|
628
|
+
const optionPromises = refFields.map(async ({ fieldName, entityName }) => {
|
|
629
|
+
const refEntity = allEntities.find((e) => e.name === entityName)
|
|
630
|
+
if (!refEntity) return { fieldName, options: [] }
|
|
631
|
+
const options = await fetchReferenceOptions(refEntity, { locale: contentLocale })
|
|
632
|
+
return { fieldName, options }
|
|
633
|
+
})
|
|
634
|
+
const results = await Promise.all(optionPromises)
|
|
635
|
+
for (const { fieldName, options } of results) {
|
|
636
|
+
referenceOptions[fieldName] = options
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return (
|
|
641
|
+
<div className="p-6">
|
|
642
|
+
<div className="mb-6">
|
|
643
|
+
<h1 className="text-2xl font-bold">New {getEntityLabelSingular(entity)}</h1>
|
|
644
|
+
</div>
|
|
645
|
+
<GenericEntityForm
|
|
646
|
+
entity={toEntityMeta(entity)}
|
|
647
|
+
locale={contentLocale}
|
|
648
|
+
referenceOptions={referenceOptions}
|
|
649
|
+
listUrl={\`/\${locale}/admin/\${urlSlug}\`}
|
|
650
|
+
/>
|
|
651
|
+
</div>
|
|
652
|
+
)
|
|
653
|
+
})
|
|
654
|
+
}
|
|
655
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/media/page.tsx`),`import { EntityList } from '@murumets-ee/admin-ui/entity-list'
|
|
656
|
+
import { fetchEntityList, toEntityMeta } from '@murumets-ee/admin-ui/server'
|
|
657
|
+
import { Media } from '@murumets-ee/media'
|
|
658
|
+
import { getMediaClient } from '@murumets-ee/media/client'
|
|
659
|
+
import { setRequestLocale } from 'next-intl/server'
|
|
660
|
+
|
|
661
|
+
export default async function MediaListPage({ params }: { params: Promise<{ locale: string }> }) {
|
|
662
|
+
const { locale } = await params
|
|
663
|
+
setRequestLocale(locale)
|
|
664
|
+
const initialData = await fetchEntityList(Media, { sortField: 'createdAt' })
|
|
665
|
+
|
|
666
|
+
const mediaClient = await getMediaClient()
|
|
667
|
+
const ids = initialData.items.map((i) => i.id as string)
|
|
668
|
+
const thumbMap = await mediaClient.getVariantUrls(ids, 'thumbnail')
|
|
669
|
+
for (const item of initialData.items) {
|
|
670
|
+
item.thumbnailUrl = thumbMap.get(item.id as string) ?? ''
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return (
|
|
674
|
+
<div className="p-6">
|
|
675
|
+
<div className="mb-6">
|
|
676
|
+
<h1 className="text-2xl font-bold">Media</h1>
|
|
677
|
+
<p className="text-sm text-muted-foreground">
|
|
678
|
+
Manage uploaded media files.
|
|
679
|
+
</p>
|
|
680
|
+
</div>
|
|
681
|
+
<EntityList
|
|
682
|
+
entity={toEntityMeta(Media)}
|
|
683
|
+
entityPath="media"
|
|
684
|
+
image={{ field: 'thumbnailUrl', size: 'sm', shape: 'rounded' }}
|
|
685
|
+
allowDelete
|
|
686
|
+
defaultSort="createdAt"
|
|
687
|
+
searchPlaceholder="Search media..."
|
|
688
|
+
editHref={\`/\${locale}/admin/media/:id\`}
|
|
689
|
+
initialData={initialData}
|
|
690
|
+
/>
|
|
691
|
+
</div>
|
|
692
|
+
)
|
|
693
|
+
}
|
|
694
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/media/[id]/page.tsx`),`import { toEntityMeta } from '@murumets-ee/admin-ui/server'
|
|
695
|
+
import { getCurrentApp } from '@murumets-ee/core'
|
|
696
|
+
import { createAdminClient } from '@murumets-ee/core/clients'
|
|
697
|
+
import { Media } from '@murumets-ee/media'
|
|
698
|
+
import { findMediaUsages } from '@murumets-ee/media/usage'
|
|
699
|
+
import { notFound } from 'next/navigation'
|
|
700
|
+
import { setRequestLocale } from 'next-intl/server'
|
|
701
|
+
import { withAdminContext } from '@/lib/with-admin-context'
|
|
702
|
+
import { MediaForm } from './media-form'
|
|
703
|
+
import { MediaUsagePanel } from './media-usage-panel'
|
|
704
|
+
|
|
705
|
+
interface MediaEditPageProps {
|
|
706
|
+
params: Promise<{ locale: string; id: string }>
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export default async function MediaEditPage({ params }: MediaEditPageProps) {
|
|
710
|
+
const { locale, id } = await params
|
|
711
|
+
setRequestLocale(locale)
|
|
712
|
+
|
|
713
|
+
return withAdminContext(locale, async ({ locale: contentLocale }) => {
|
|
714
|
+
const app = getCurrentApp()!
|
|
715
|
+
const client = createAdminClient(Media)
|
|
716
|
+
const [mediaItem, usages] = await Promise.all([
|
|
717
|
+
client.findById(id, { locale: contentLocale }),
|
|
718
|
+
findMediaUsages(id, app.db.readWrite),
|
|
719
|
+
])
|
|
720
|
+
if (!mediaItem) notFound()
|
|
721
|
+
|
|
722
|
+
return (
|
|
723
|
+
<div className="p-6">
|
|
724
|
+
<div className="mb-6">
|
|
725
|
+
<h1 className="text-2xl font-bold">Edit Media</h1>
|
|
726
|
+
</div>
|
|
727
|
+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
728
|
+
<div className="lg:col-span-2">
|
|
729
|
+
<MediaForm
|
|
730
|
+
entity={toEntityMeta(Media)}
|
|
731
|
+
id={id}
|
|
732
|
+
locale={contentLocale}
|
|
733
|
+
initialData={mediaItem as Record<string, unknown>}
|
|
734
|
+
/>
|
|
735
|
+
</div>
|
|
736
|
+
<div>
|
|
737
|
+
<MediaUsagePanel usages={usages} locale={locale} />
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
</div>
|
|
741
|
+
)
|
|
742
|
+
})
|
|
743
|
+
}
|
|
744
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/media/[id]/media-form.tsx`),`'use client'
|
|
745
|
+
|
|
746
|
+
import { EntityForm } from '@murumets-ee/admin-ui/entity-form'
|
|
747
|
+
import type { EntityMeta } from '@murumets-ee/admin-ui/entity-list'
|
|
748
|
+
import { useRouter } from 'next/navigation'
|
|
749
|
+
|
|
750
|
+
interface MediaFormProps {
|
|
751
|
+
entity: EntityMeta
|
|
752
|
+
id: string
|
|
753
|
+
locale?: string
|
|
754
|
+
initialData?: Record<string, unknown>
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export function MediaForm({ entity, id, locale, initialData }: MediaFormProps) {
|
|
758
|
+
const router = useRouter()
|
|
759
|
+
|
|
760
|
+
return (
|
|
761
|
+
<EntityForm
|
|
762
|
+
entity={entity}
|
|
763
|
+
id={id}
|
|
764
|
+
locale={locale}
|
|
765
|
+
initialData={initialData}
|
|
766
|
+
entityPath="media"
|
|
767
|
+
layout="two-column"
|
|
768
|
+
fields={['title', 'alt', 'description', 'filename', 'mimeType', 'size', 'mediaType']}
|
|
769
|
+
fieldOverrides={{
|
|
770
|
+
filename: { readOnly: true, description: 'Original upload filename (read-only)' },
|
|
771
|
+
mimeType: { readOnly: true, label: 'MIME Type' },
|
|
772
|
+
size: { readOnly: true, description: 'File size in bytes' },
|
|
773
|
+
mediaType: { readOnly: true, label: 'Media Type' },
|
|
774
|
+
alt: { label: 'Alt Text', description: 'Alternative text for accessibility' },
|
|
775
|
+
}}
|
|
776
|
+
onSuccess={() => router.push(\`/\${locale}/admin/media\`)}
|
|
777
|
+
onCancel={() => router.back()}
|
|
778
|
+
/>
|
|
779
|
+
)
|
|
780
|
+
}
|
|
781
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/media/[id]/media-usage-panel.tsx`),`import type { MediaUsage } from '@murumets-ee/media/usage'
|
|
782
|
+
|
|
783
|
+
interface MediaUsagePanelProps {
|
|
784
|
+
usages: MediaUsage[]
|
|
785
|
+
locale: string
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
export function MediaUsagePanel({ usages, locale }: MediaUsagePanelProps) {
|
|
789
|
+
return (
|
|
790
|
+
<div className="rounded-lg border bg-card p-4">
|
|
791
|
+
<h3 className="mb-3 text-sm font-semibold">Used by</h3>
|
|
792
|
+
|
|
793
|
+
{usages.length === 0 && (
|
|
794
|
+
<p className="text-sm text-muted-foreground">
|
|
795
|
+
This media item is not referenced by any entity.
|
|
796
|
+
</p>
|
|
797
|
+
)}
|
|
798
|
+
|
|
799
|
+
{usages.length > 0 && (
|
|
800
|
+
<ul className="space-y-2">
|
|
801
|
+
{usages.map((usage) => {
|
|
802
|
+
const entitySlug = \`\${usage.entityName}s\`
|
|
803
|
+
const href = \`/\${locale}/admin/\${entitySlug}/\${usage.entityId}\`
|
|
804
|
+
|
|
805
|
+
return (
|
|
806
|
+
<li key={\`\${usage.entityName}-\${usage.entityId}-\${usage.fieldName}\`}>
|
|
807
|
+
<a
|
|
808
|
+
href={href}
|
|
809
|
+
className="block rounded-md border px-3 py-2 text-sm hover:bg-muted transition-colors"
|
|
810
|
+
>
|
|
811
|
+
<span className="font-medium capitalize">{usage.entityName}</span>
|
|
812
|
+
<span className="mx-1.5 text-muted-foreground">·</span>
|
|
813
|
+
<span className="text-muted-foreground">{usage.fieldName}</span>
|
|
814
|
+
{usage.context === 'block' && (
|
|
815
|
+
<span className="ml-1.5 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
|
|
816
|
+
block
|
|
817
|
+
</span>
|
|
818
|
+
)}
|
|
819
|
+
</a>
|
|
820
|
+
</li>
|
|
821
|
+
)
|
|
822
|
+
})}
|
|
823
|
+
</ul>
|
|
824
|
+
)}
|
|
825
|
+
</div>
|
|
826
|
+
)
|
|
827
|
+
}
|
|
828
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/media/image-styles/page.tsx`),`import { imageStylesSettings } from '@murumets-ee/media'
|
|
829
|
+
import { ImageStylesManager } from '@murumets-ee/media/image-styles'
|
|
830
|
+
import { createSettingsClient } from '@murumets-ee/settings'
|
|
831
|
+
import { setRequestLocale } from 'next-intl/server'
|
|
832
|
+
import { getToolkitApp } from '@/lib/app'
|
|
833
|
+
|
|
834
|
+
async function loadImageStyles() {
|
|
835
|
+
const app = await getToolkitApp()
|
|
836
|
+
const client = createSettingsClient(imageStylesSettings, { app })
|
|
837
|
+
const styles = await client.get('imageStyles')
|
|
838
|
+
return styles ?? {}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
export default async function ImageStylesPage({ params }: { params: Promise<{ locale: string }> }) {
|
|
842
|
+
const { locale } = await params
|
|
843
|
+
setRequestLocale(locale)
|
|
844
|
+
|
|
845
|
+
const initialStyles = await loadImageStyles()
|
|
846
|
+
|
|
847
|
+
return (
|
|
848
|
+
<div className="p-6 max-w-4xl">
|
|
849
|
+
<ImageStylesManager initialStyles={initialStyles} />
|
|
850
|
+
</div>
|
|
851
|
+
)
|
|
852
|
+
}
|
|
853
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/users/page.tsx`),`import type { UsersInitialData } from '@murumets-ee/admin-ui/users'
|
|
854
|
+
import { getAuth } from '@murumets-ee/auth'
|
|
855
|
+
import { headers } from 'next/headers'
|
|
856
|
+
import { setRequestLocale } from 'next-intl/server'
|
|
857
|
+
import { getToolkitApp } from '@/lib/app'
|
|
858
|
+
import { UsersPage } from './users-page'
|
|
859
|
+
|
|
860
|
+
export default async function UsersAdminPage({ params }: { params: Promise<{ locale: string }> }) {
|
|
861
|
+
const { locale } = await params
|
|
862
|
+
setRequestLocale(locale)
|
|
863
|
+
|
|
864
|
+
await getToolkitApp()
|
|
865
|
+
const auth = getAuth()
|
|
866
|
+
|
|
867
|
+
const result = await auth.api.listUsers({
|
|
868
|
+
headers: await headers(),
|
|
869
|
+
query: { limit: 20, sortBy: 'createdAt', sortDirection: 'desc' },
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
const initialData: UsersInitialData = {
|
|
873
|
+
users: result.users.map((u) => ({
|
|
874
|
+
id: u.id,
|
|
875
|
+
name: u.name,
|
|
876
|
+
email: u.email,
|
|
877
|
+
emailVerified: u.emailVerified,
|
|
878
|
+
image: u.image ?? null,
|
|
879
|
+
createdAt: u.createdAt instanceof Date ? u.createdAt.toISOString() : String(u.createdAt),
|
|
880
|
+
updatedAt: u.updatedAt instanceof Date ? u.updatedAt.toISOString() : String(u.updatedAt),
|
|
881
|
+
role: u.role ?? null,
|
|
882
|
+
banned: u.banned ?? null,
|
|
883
|
+
banReason: u.banReason ?? null,
|
|
884
|
+
banExpires:
|
|
885
|
+
u.banExpires instanceof Date
|
|
886
|
+
? u.banExpires.toISOString()
|
|
887
|
+
: u.banExpires
|
|
888
|
+
? String(u.banExpires)
|
|
889
|
+
: null,
|
|
890
|
+
})),
|
|
891
|
+
total: result.total,
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return (
|
|
895
|
+
<div className="p-6">
|
|
896
|
+
<div className="mb-6">
|
|
897
|
+
<h1 className="text-2xl font-bold">Users</h1>
|
|
898
|
+
<p className="text-sm text-muted-foreground">Manage user accounts, roles, and access.</p>
|
|
899
|
+
</div>
|
|
900
|
+
<UsersPage initialData={initialData} />
|
|
901
|
+
</div>
|
|
902
|
+
)
|
|
903
|
+
}
|
|
904
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/users/users-page.tsx`),`'use client'
|
|
905
|
+
|
|
906
|
+
import type { UsersInitialData } from '@murumets-ee/admin-ui/users'
|
|
907
|
+
import { UsersManagement } from '@murumets-ee/admin-ui/users'
|
|
908
|
+
import { createUsersApi } from '@murumets-ee/auth/client'
|
|
909
|
+
import { authClient } from '@/lib/auth-client'
|
|
910
|
+
|
|
911
|
+
const usersApi = createUsersApi(authClient)
|
|
912
|
+
|
|
913
|
+
export function UsersPage({ initialData }: { initialData: UsersInitialData }) {
|
|
914
|
+
const session = authClient.useSession()
|
|
915
|
+
|
|
916
|
+
return (
|
|
917
|
+
<UsersManagement
|
|
918
|
+
api={usersApi}
|
|
919
|
+
currentUserId={session.data?.user?.id}
|
|
920
|
+
roles={['admin', 'editor', 'viewer']}
|
|
921
|
+
initialData={initialData}
|
|
922
|
+
/>
|
|
923
|
+
)
|
|
924
|
+
}
|
|
925
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/permissions/page.tsx`),`import { PermissionsEditor } from '@murumets-ee/admin-ui/permissions'
|
|
926
|
+
import { BUILT_IN_ROLES, buildResourceCatalog } from '@murumets-ee/auth'
|
|
927
|
+
import { setRequestLocale } from 'next-intl/server'
|
|
928
|
+
import { allEntities, pluginResources } from '@/lib/admin-config'
|
|
929
|
+
import { getToolkitApp } from '@/lib/app'
|
|
930
|
+
import { loadRoles } from '@/lib/load-roles'
|
|
931
|
+
|
|
932
|
+
export default async function PermissionsPage({ params }: { params: Promise<{ locale: string }> }) {
|
|
933
|
+
const { locale } = await params
|
|
934
|
+
setRequestLocale(locale)
|
|
935
|
+
|
|
936
|
+
const app = await getToolkitApp()
|
|
937
|
+
const savedRoles = await loadRoles(app)
|
|
938
|
+
const statements = buildResourceCatalog(allEntities, pluginResources)
|
|
939
|
+
|
|
940
|
+
return (
|
|
941
|
+
<div className="p-6">
|
|
942
|
+
<div className="mb-6">
|
|
943
|
+
<h1 className="text-2xl font-bold">Permissions</h1>
|
|
944
|
+
<p className="text-sm text-muted-foreground">
|
|
945
|
+
Configure what each role can do. Admin always has full access.
|
|
946
|
+
</p>
|
|
947
|
+
</div>
|
|
948
|
+
<PermissionsEditor
|
|
949
|
+
statements={statements}
|
|
950
|
+
roles={Object.keys(savedRoles)}
|
|
951
|
+
builtInRoles={[...BUILT_IN_ROLES]}
|
|
952
|
+
initialPermissions={savedRoles}
|
|
953
|
+
/>
|
|
954
|
+
</div>
|
|
955
|
+
)
|
|
956
|
+
}
|
|
957
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/roles/page.tsx`),`import { RolesEditor } from '@murumets-ee/admin-ui/permissions'
|
|
958
|
+
import { BUILT_IN_ROLES } from '@murumets-ee/auth'
|
|
959
|
+
import { setRequestLocale } from 'next-intl/server'
|
|
960
|
+
import { getToolkitApp } from '@/lib/app'
|
|
961
|
+
import { loadRoles } from '@/lib/load-roles'
|
|
962
|
+
|
|
963
|
+
export default async function RolesPage({ params }: { params: Promise<{ locale: string }> }) {
|
|
964
|
+
const { locale } = await params
|
|
965
|
+
setRequestLocale(locale)
|
|
966
|
+
|
|
967
|
+
const app = await getToolkitApp()
|
|
968
|
+
const savedRoles = await loadRoles(app)
|
|
969
|
+
|
|
970
|
+
const roles = [
|
|
971
|
+
{ name: 'admin', builtIn: true, permissionCount: 0 },
|
|
972
|
+
...Object.entries(savedRoles).map(([name, perms]) => ({
|
|
973
|
+
name,
|
|
974
|
+
builtIn: (BUILT_IN_ROLES as readonly string[]).includes(name),
|
|
975
|
+
permissionCount: Object.values(perms).flat().length,
|
|
976
|
+
})),
|
|
977
|
+
]
|
|
978
|
+
|
|
979
|
+
return (
|
|
980
|
+
<div className="p-6">
|
|
981
|
+
<div className="mb-6">
|
|
982
|
+
<h1 className="text-2xl font-bold">Roles</h1>
|
|
983
|
+
<p className="text-sm text-muted-foreground">
|
|
984
|
+
Manage user roles. Built-in roles cannot be deleted.
|
|
985
|
+
</p>
|
|
986
|
+
</div>
|
|
987
|
+
<RolesEditor roles={roles} permissionsHref={\`/\${locale}/admin/permissions\`} />
|
|
988
|
+
</div>
|
|
989
|
+
)
|
|
990
|
+
}
|
|
991
|
+
`),await n(a(e,`app/[locale]/(shell)/admin/activity/page.tsx`),`import { AuditLog } from '@murumets-ee/admin-ui/audit-log'
|
|
992
|
+
import { fetchAuditLogData } from '@murumets-ee/admin-ui/server'
|
|
993
|
+
import { setRequestLocale } from 'next-intl/server'
|
|
994
|
+
import { withAdminContext } from '@/lib/with-admin-context'
|
|
995
|
+
|
|
996
|
+
export default async function ActivityPage({ params }: { params: Promise<{ locale: string }> }) {
|
|
997
|
+
const { locale } = await params
|
|
998
|
+
setRequestLocale(locale)
|
|
999
|
+
|
|
1000
|
+
return withAdminContext(locale, async () => {
|
|
1001
|
+
const initialData = await fetchAuditLogData()
|
|
1002
|
+
|
|
1003
|
+
return (
|
|
1004
|
+
<div className="p-6">
|
|
1005
|
+
<div className="mb-6">
|
|
1006
|
+
<h1 className="text-2xl font-bold">Activity Log</h1>
|
|
1007
|
+
<p className="text-sm text-muted-foreground">
|
|
1008
|
+
Track all content changes across your CMS.
|
|
1009
|
+
</p>
|
|
1010
|
+
</div>
|
|
1011
|
+
<AuditLog
|
|
1012
|
+
initialData={initialData}
|
|
1013
|
+
editHrefPattern={\`/\${locale}/admin/:entityType/:entityId\`}
|
|
1014
|
+
/>
|
|
1015
|
+
</div>
|
|
1016
|
+
)
|
|
1017
|
+
})
|
|
1018
|
+
}
|
|
1019
|
+
`),await n(a(e,`app/api/admin/[...path]/route.ts`),`import { createAdminApiHandler } from '@murumets-ee/admin-ui/server'
|
|
1020
|
+
import { buildPermissionChecker, buildResourceCatalog } from '@murumets-ee/auth'
|
|
1021
|
+
import { permissionRoutes } from '@murumets-ee/auth/admin'
|
|
1022
|
+
import { ContentClient } from '@murumets-ee/content/client'
|
|
1023
|
+
import { LockService } from '@murumets-ee/content/lock'
|
|
1024
|
+
import type { PermissionChecker } from '@murumets-ee/core'
|
|
1025
|
+
import { getApp } from '@murumets-ee/core'
|
|
1026
|
+
import {
|
|
1027
|
+
AuditLogClient,
|
|
1028
|
+
createAuditDbWriter,
|
|
1029
|
+
createAuditLogger,
|
|
1030
|
+
createLogger,
|
|
1031
|
+
} from '@murumets-ee/logging'
|
|
1032
|
+
import { logRoutes } from '@murumets-ee/logging/admin'
|
|
1033
|
+
import { mediaRoutes } from '@murumets-ee/media/admin'
|
|
1034
|
+
import { createSettingsClient } from '@murumets-ee/settings'
|
|
1035
|
+
import { settingsRoutes } from '@murumets-ee/settings/admin'
|
|
1036
|
+
import { storageRoutes } from '@murumets-ee/storage/admin'
|
|
1037
|
+
import { taxonomyRoutes } from '@murumets-ee/taxonomy/admin'
|
|
1038
|
+
import {
|
|
1039
|
+
allEntities,
|
|
1040
|
+
crudEntities,
|
|
1041
|
+
pluginResources,
|
|
1042
|
+
taxonomyVocabularies,
|
|
1043
|
+
} from '@/lib/admin-config'
|
|
1044
|
+
import { getToolkitApp } from '@/lib/app'
|
|
1045
|
+
import { auth } from '@/lib/auth'
|
|
1046
|
+
import { loadRoles } from '@/lib/load-roles'
|
|
1047
|
+
import { permissionSettings } from '@/settings/permissions'
|
|
1048
|
+
import { siteSettings } from '@/settings/site'
|
|
1049
|
+
|
|
1050
|
+
await getToolkitApp()
|
|
1051
|
+
|
|
1052
|
+
let _auditLogger: ReturnType<typeof createAuditLogger> | null = null
|
|
1053
|
+
let _checker: PermissionChecker | null = null
|
|
1054
|
+
|
|
1055
|
+
const routes = [
|
|
1056
|
+
mediaRoutes(),
|
|
1057
|
+
storageRoutes(),
|
|
1058
|
+
settingsRoutes(siteSettings),
|
|
1059
|
+
taxonomyRoutes(taxonomyVocabularies),
|
|
1060
|
+
logRoutes(() => new AuditLogClient(getApp().db.readWrite)),
|
|
1061
|
+
permissionRoutes({
|
|
1062
|
+
getStatements: () => buildResourceCatalog(allEntities, pluginResources),
|
|
1063
|
+
loadRoles: async () => loadRoles(getApp()),
|
|
1064
|
+
saveRoles: async (roles) => {
|
|
1065
|
+
const app = getApp()
|
|
1066
|
+
const client = createSettingsClient(permissionSettings, { app })
|
|
1067
|
+
await client.set('roles', roles)
|
|
1068
|
+
},
|
|
1069
|
+
onSave: () => {
|
|
1070
|
+
_checker = null
|
|
1071
|
+
},
|
|
1072
|
+
}),
|
|
1073
|
+
]
|
|
1074
|
+
|
|
1075
|
+
const handler = createAdminApiHandler({
|
|
1076
|
+
authenticate: async (req) => {
|
|
1077
|
+
const session = await auth.api.getSession({ headers: req.headers })
|
|
1078
|
+
if (!session?.user) return null
|
|
1079
|
+
const role = (session.user as Record<string, unknown>).role as string | undefined
|
|
1080
|
+
return { id: session.user.id, role, name: session.user.name, email: session.user.email }
|
|
1081
|
+
},
|
|
1082
|
+
defaultLocale: 'en',
|
|
1083
|
+
entities: crudEntities,
|
|
1084
|
+
routes,
|
|
1085
|
+
loadPermissions: async () => {
|
|
1086
|
+
if (_checker) return _checker
|
|
1087
|
+
const roles = await loadRoles(getApp())
|
|
1088
|
+
_checker = buildPermissionChecker(roles)
|
|
1089
|
+
return _checker
|
|
1090
|
+
},
|
|
1091
|
+
auditLogger: {
|
|
1092
|
+
log: async (entry) => {
|
|
1093
|
+
if (!_auditLogger) {
|
|
1094
|
+
const app = getApp()
|
|
1095
|
+
_auditLogger = createAuditLogger({
|
|
1096
|
+
logger: createLogger({ name: 'audit' }),
|
|
1097
|
+
dbWriter: createAuditDbWriter(app.db.readWrite),
|
|
1098
|
+
})
|
|
1099
|
+
}
|
|
1100
|
+
return _auditLogger.log(entry)
|
|
1101
|
+
},
|
|
1102
|
+
},
|
|
1103
|
+
contentClientFactory: (entity) =>
|
|
1104
|
+
new ContentClient({ entity, db: getApp().db.readWrite }),
|
|
1105
|
+
lockServiceFactory: () =>
|
|
1106
|
+
new LockService({ db: getApp().db.readWrite }),
|
|
1107
|
+
})
|
|
1108
|
+
|
|
1109
|
+
export const { GET, POST, PATCH, DELETE } = handler
|
|
1110
|
+
`)}async function c(e,t){await n(a(e,`lib/auth.ts`),`import { getToolkitApp } from './app'
|
|
2
1111
|
import { getAuth } from '@murumets-ee/auth'
|
|
3
1112
|
|
|
4
1113
|
export async function getAuthInstance() {
|
|
5
1114
|
await getToolkitApp()
|
|
6
1115
|
return getAuth()
|
|
7
1116
|
}
|
|
8
|
-
`),await
|
|
1117
|
+
`),await n(a(e,`lib/auth-client.ts`),`import { createClient } from '@murumets-ee/auth/client'
|
|
9
1118
|
|
|
10
1119
|
export const authClient = createClient()
|
|
11
|
-
`),await
|
|
1120
|
+
`),await n(a(e,`app/api/auth/[...all]/route.ts`),`import { toNextJsHandler } from 'better-auth/next-js'
|
|
12
1121
|
import { getAuthInstance } from '../../../../lib/auth'
|
|
13
1122
|
|
|
14
1123
|
let _handler: ReturnType<typeof toNextJsHandler> | null = null
|
|
@@ -30,7 +1139,7 @@ export async function POST(req: Request) {
|
|
|
30
1139
|
const h = await handler()
|
|
31
1140
|
return h.POST(req)
|
|
32
1141
|
}
|
|
33
|
-
`),await
|
|
1142
|
+
`),await n(a(e,`app/[locale]/auth/layout.tsx`),`import { AuthProviders } from './providers'
|
|
34
1143
|
import type { ReactNode } from 'react'
|
|
35
1144
|
|
|
36
1145
|
export default function AuthLayout({ children }: { children: ReactNode }) {
|
|
@@ -42,7 +1151,7 @@ export default function AuthLayout({ children }: { children: ReactNode }) {
|
|
|
42
1151
|
</AuthProviders>
|
|
43
1152
|
)
|
|
44
1153
|
}
|
|
45
|
-
`),await
|
|
1154
|
+
`),await n(a(e,`app/[locale]/auth/providers.tsx`),`'use client'
|
|
46
1155
|
|
|
47
1156
|
import { AuthUIProvider } from '@murumets-ee/auth-ui'
|
|
48
1157
|
import { authClient } from '@/lib/auth-client'
|
|
@@ -68,22 +1177,22 @@ export function AuthProviders({ children }: { children: ReactNode }) {
|
|
|
68
1177
|
</AuthUIProvider>
|
|
69
1178
|
)
|
|
70
1179
|
}
|
|
71
|
-
`),await
|
|
1180
|
+
`),await n(a(e,`app/[locale]/auth/sign-in/page.tsx`),`import { SignInForm } from '@murumets-ee/auth-ui'
|
|
72
1181
|
|
|
73
1182
|
export default function SignInPage() {
|
|
74
1183
|
return <SignInForm />
|
|
75
1184
|
}
|
|
76
|
-
`),await
|
|
1185
|
+
`),await n(a(e,`app/[locale]/auth/sign-up/page.tsx`),`import { SignUpForm } from '@murumets-ee/auth-ui'
|
|
77
1186
|
|
|
78
1187
|
export default function SignUpPage() {
|
|
79
1188
|
return <SignUpForm />
|
|
80
1189
|
}
|
|
81
|
-
`),await
|
|
1190
|
+
`),await n(a(e,`app/[locale]/auth/forgot-password/page.tsx`),`import { ForgotPasswordForm } from '@murumets-ee/auth-ui'
|
|
82
1191
|
|
|
83
1192
|
export default function ForgotPasswordPage() {
|
|
84
1193
|
return <ForgotPasswordForm />
|
|
85
1194
|
}
|
|
86
|
-
`),await
|
|
1195
|
+
`),await n(a(e,`app/[locale]/auth/reset-password/page.tsx`),`import { Suspense } from 'react'
|
|
87
1196
|
import { ResetPasswordContent } from './content'
|
|
88
1197
|
|
|
89
1198
|
export default function ResetPasswordPage() {
|
|
@@ -93,7 +1202,7 @@ export default function ResetPasswordPage() {
|
|
|
93
1202
|
</Suspense>
|
|
94
1203
|
)
|
|
95
1204
|
}
|
|
96
|
-
`),await
|
|
1205
|
+
`),await n(a(e,`app/[locale]/auth/reset-password/content.tsx`),`'use client'
|
|
97
1206
|
|
|
98
1207
|
import { useSearchParams } from 'next/navigation'
|
|
99
1208
|
import { ResetPasswordForm } from '@murumets-ee/auth-ui'
|
|
@@ -117,7 +1226,7 @@ export function ResetPasswordContent() {
|
|
|
117
1226
|
|
|
118
1227
|
return <ResetPasswordForm token={token} />
|
|
119
1228
|
}
|
|
120
|
-
`),await
|
|
1229
|
+
`),await n(a(e,`app/[locale]/setup/page.tsx`),`import { redirect } from 'next/navigation'
|
|
121
1230
|
import { sql } from 'drizzle-orm'
|
|
122
1231
|
import { getToolkitApp } from '@/lib/app'
|
|
123
1232
|
import { SetupForm } from './form'
|
|
@@ -142,7 +1251,7 @@ export default async function SetupPage() {
|
|
|
142
1251
|
</div>
|
|
143
1252
|
)
|
|
144
1253
|
}
|
|
145
|
-
`),await
|
|
1254
|
+
`),await n(a(e,`app/[locale]/setup/form.tsx`),`'use client'
|
|
146
1255
|
|
|
147
1256
|
import { useState } from 'react'
|
|
148
1257
|
import { createFirstAdmin } from './actions'
|
|
@@ -213,7 +1322,7 @@ export function SetupForm() {
|
|
|
213
1322
|
</form>
|
|
214
1323
|
)
|
|
215
1324
|
}
|
|
216
|
-
`),await
|
|
1325
|
+
`),await n(a(e,`app/[locale]/setup/actions.ts`),`'use server'
|
|
217
1326
|
|
|
218
1327
|
import { sql } from 'drizzle-orm'
|
|
219
1328
|
import { getToolkitApp } from '@/lib/app'
|
|
@@ -267,17 +1376,19 @@ export async function createFirstAdmin(formData: FormData) {
|
|
|
267
1376
|
|
|
268
1377
|
return { ok: true, email: adminUser.user.email }
|
|
269
1378
|
}
|
|
270
|
-
`)}
|
|
1379
|
+
`)}async function l(e,t){let{name:r}=t;await n(a(e,`.env.example`),`DATABASE_URL=postgresql://${r}:${r}_dev_password@localhost:5432/${r}_dev
|
|
271
1380
|
BETTER_AUTH_SECRET=dev-secret-change-me-in-production-min-32-chars
|
|
272
1381
|
BETTER_AUTH_URL=http://localhost:3000
|
|
273
1382
|
LOG_LEVEL=debug
|
|
274
1383
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
275
|
-
|
|
1384
|
+
# Queue worker — by default runs embedded in the web process.
|
|
1385
|
+
# Set to "false" in production and run "pnpm worker" as a separate process.
|
|
1386
|
+
# QUEUE_WORKER=false
|
|
276
1387
|
RESEND_API_KEY=
|
|
277
1388
|
RESEND_WEBHOOK_SECRET=
|
|
278
1389
|
MAIL_FROM=noreply@example.com
|
|
279
1390
|
CSAT_SECRET=
|
|
280
|
-
`),await
|
|
1391
|
+
`),await n(a(e,`.dockerignore`),`# Dependencies (installed inside Docker)
|
|
281
1392
|
node_modules/
|
|
282
1393
|
.pnpm-store/
|
|
283
1394
|
|
|
@@ -321,8 +1432,8 @@ docs/
|
|
|
321
1432
|
# OS files
|
|
322
1433
|
.DS_Store
|
|
323
1434
|
Thumbs.db
|
|
324
|
-
`),await
|
|
325
|
-
# Node 22 LTS (Debian slim
|
|
1435
|
+
`),await n(a(e,`Dockerfile`),`# --- Base image ---
|
|
1436
|
+
# Node 22 LTS (Debian slim — glibc required for sharp image optimization)
|
|
326
1437
|
FROM node:22-slim AS base
|
|
327
1438
|
|
|
328
1439
|
# --- Stage 1: Install dependencies ---
|
|
@@ -356,7 +1467,7 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
|
|
356
1467
|
|
|
357
1468
|
# Copy standalone output (includes server.js + traced node_modules)
|
|
358
1469
|
COPY --from=builder --chown=node:node /app/.next/standalone ./
|
|
359
|
-
# Copy static assets (JS/CSS chunks
|
|
1470
|
+
# Copy static assets (JS/CSS chunks — excluded from standalone trace)
|
|
360
1471
|
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
|
|
361
1472
|
# Copy public assets (favicon, robots.txt, images)
|
|
362
1473
|
COPY --from=builder --chown=node:node /app/public ./public
|
|
@@ -367,7 +1478,7 @@ COPY --from=builder --chown=node:node /app/migrations ./migrations
|
|
|
367
1478
|
USER node
|
|
368
1479
|
EXPOSE 3000
|
|
369
1480
|
CMD ["node", "server.js"]
|
|
370
|
-
`),await
|
|
1481
|
+
`),await n(a(e,`docker-compose.yml`),`services:
|
|
371
1482
|
postgres:
|
|
372
1483
|
image: postgres:17-alpine
|
|
373
1484
|
container_name: ${r}-postgres
|
|
@@ -389,7 +1500,7 @@ CMD ["node", "server.js"]
|
|
|
389
1500
|
volumes:
|
|
390
1501
|
postgres_data:
|
|
391
1502
|
name: ${r}_postgres_data
|
|
392
|
-
`),await
|
|
1503
|
+
`),await n(a(e,`docker-compose.prod.yml`),`services:
|
|
393
1504
|
app:
|
|
394
1505
|
build: .
|
|
395
1506
|
restart: unless-stopped
|
|
@@ -424,16 +1535,16 @@ volumes:
|
|
|
424
1535
|
|
|
425
1536
|
networks:
|
|
426
1537
|
internal:
|
|
427
|
-
`),await
|
|
1538
|
+
`),await i(a(e,`.gitignore`),`
|
|
428
1539
|
# Toolkit generated files
|
|
429
1540
|
generated/
|
|
430
1541
|
|
|
431
1542
|
# Environment
|
|
432
1543
|
.env*
|
|
433
1544
|
!.env.example
|
|
434
|
-
`)}
|
|
1545
|
+
`)}async function u(e,t){await n(a(e,`entities/index.ts`),`export { Article } from './article'
|
|
435
1546
|
export { Category } from './category'
|
|
436
|
-
`),await
|
|
1547
|
+
`),await n(a(e,`entities/article.ts`),`import { behavior, defineEntity, field } from '@murumets-ee/entity'
|
|
437
1548
|
|
|
438
1549
|
export const Article = defineEntity({
|
|
439
1550
|
name: 'article',
|
|
@@ -464,7 +1575,7 @@ export const Article = defineEntity({
|
|
|
464
1575
|
delete: 'group.admin',
|
|
465
1576
|
},
|
|
466
1577
|
})
|
|
467
|
-
`),await
|
|
1578
|
+
`),await n(a(e,`entities/category.ts`),`import { defineEntity, field } from '@murumets-ee/entity'
|
|
468
1579
|
|
|
469
1580
|
export const Category = defineEntity({
|
|
470
1581
|
name: 'category',
|
|
@@ -481,13 +1592,13 @@ export const Category = defineEntity({
|
|
|
481
1592
|
delete: 'group.admin',
|
|
482
1593
|
},
|
|
483
1594
|
})
|
|
484
|
-
`)}
|
|
1595
|
+
`)}async function d(e,t){await n(a(e,`i18n/routing.ts`),`import { defineRouting } from 'next-intl/routing'
|
|
485
1596
|
|
|
486
1597
|
export const routing = defineRouting({
|
|
487
1598
|
locales: ['en'],
|
|
488
1599
|
defaultLocale: 'en',
|
|
489
1600
|
})
|
|
490
|
-
`),await
|
|
1601
|
+
`),await n(a(e,`i18n/request.ts`),`import { getRequestConfig } from 'next-intl/server'
|
|
491
1602
|
import { hasLocale } from 'next-intl'
|
|
492
1603
|
import { routing } from './routing'
|
|
493
1604
|
import { getAuthMessages } from '@murumets-ee/auth-ui/i18n'
|
|
@@ -507,15 +1618,14 @@ export default getRequestConfig(async ({ requestLocale }) => {
|
|
|
507
1618
|
},
|
|
508
1619
|
}
|
|
509
1620
|
})
|
|
510
|
-
`)}
|
|
1621
|
+
`)}const f={myorgCore:`0.1.0`,myorgDb:`0.1.0`,myorgEntity:`0.1.0`,myorgLogging:`0.1.0`,myorgAuth:`0.1.0`,myorgAuthUi:`0.1.0`,myorgAdminUi:`0.1.0`,myorgContent:`0.1.0`,myorgContentApi:`0.1.0`,myorgSettings:`0.1.0`,myorgStorage:`0.1.0`,myorgMedia:`0.1.0`,myorgTaxonomy:`0.1.0`,myorgQueue:`0.1.0`,myorgMail:`0.1.0`,myorgTicketing:`0.1.0`,myorgTicketingUi:`0.1.0`,myorgEditor:`0.1.0`,myorgBlocks:`0.1.0`,myorgTokens:`0.1.0`,myorgUi:`0.1.0`,myorgCli:`0.1.0`,betterAuth:`1.4.0`,drizzleOrm:`0.45.1`,nextIntl:`4.8.2`,nextThemes:`0.4.6`,lucideReact:`0.563.0`,postgres:`3.4.5`,reactHookForm:`7.71.1`,hookformResolvers:`5.2.2`,zod:`3.24.1`,tanstackReactQuery:`5.60.5`,tanstackReactTable:`8.21.3`,drizzleKit:`0.31.10`,tsx:`4.19.2`,babelReactCompiler:`1.0.0`};async function p(e,t,n){await m(e,t),n?.(`Workspace root created`),n?.(`Creating admin app...`),await g(e,t),n?.(`Admin app created`),n?.(`Creating web app...`),await _(e,t),n?.(`Web app created`),await h(e,t),n?.(`Shared config package created`)}async function m(e,t){let{name:r}=t;await o(e,{recursive:!0}),await n(a(e,`pnpm-workspace.yaml`),`packages:
|
|
511
1622
|
- 'packages/*'
|
|
512
1623
|
- 'apps/*'
|
|
513
1624
|
|
|
514
1625
|
ignoredBuiltDependencies:
|
|
515
1626
|
- sharp
|
|
516
1627
|
- unrs-resolver
|
|
517
|
-
`),await
|
|
518
|
-
`),await e(o(t,".gitignore"),`# dependencies
|
|
1628
|
+
`),await n(a(e,`package.json`),`${JSON.stringify({name:`@${r}/monorepo`,version:`0.0.0`,private:!0,type:`module`,scripts:{dev:`turbo dev`,build:`turbo build`,"db:generate":`tsx --env-file=.env packages/config/scripts/generate-schema.ts`,"db:migrate:generate":`drizzle-kit generate --config packages/config/drizzle.config.ts`,"db:migrate":`tsx --env-file=.env packages/config/scripts/migrate.ts`,"db:reset":`tsx --env-file=.env packages/config/scripts/reset-db.ts`},devDependencies:{turbo:`^2.3.3`,typescript:`^5.7.3`,tsx:`^${f.tsx}`,"drizzle-kit":`^${f.drizzleKit}`}},null,2)}\n`),await n(a(e,`.gitignore`),`# dependencies
|
|
519
1629
|
node_modules/
|
|
520
1630
|
|
|
521
1631
|
# build
|
|
@@ -546,7 +1656,7 @@ migrations/
|
|
|
546
1656
|
|
|
547
1657
|
# turbo
|
|
548
1658
|
.turbo/
|
|
549
|
-
`),await
|
|
1659
|
+
`),await n(a(e,`.env.example`),`DATABASE_URL=postgresql://${r}:${r}_dev_password@localhost:5432/${r}_dev
|
|
550
1660
|
BETTER_AUTH_SECRET=dev-secret-change-me-in-production-min-32-chars
|
|
551
1661
|
BETTER_AUTH_URL=http://localhost:3000
|
|
552
1662
|
LOG_LEVEL=debug
|
|
@@ -556,7 +1666,7 @@ RESEND_API_KEY=
|
|
|
556
1666
|
RESEND_WEBHOOK_SECRET=
|
|
557
1667
|
MAIL_FROM=noreply@example.com
|
|
558
1668
|
CSAT_SECRET=
|
|
559
|
-
`),await
|
|
1669
|
+
`),await n(a(e,`docker-compose.yml`),`services:
|
|
560
1670
|
postgres:
|
|
561
1671
|
image: postgres:17-alpine
|
|
562
1672
|
container_name: ${r}-postgres
|
|
@@ -578,9 +1688,7 @@ CSAT_SECRET=
|
|
|
578
1688
|
volumes:
|
|
579
1689
|
postgres_data:
|
|
580
1690
|
name: ${r}_postgres_data
|
|
581
|
-
`)}async function
|
|
582
|
-
`),await e(o(a,"tsconfig.json"),`${JSON.stringify({compilerOptions:{target:"ES2022",module:"ESNext",moduleResolution:"Bundler",strict:!0,esModuleInterop:!0,skipLibCheck:!0,resolveJsonModule:!0},include:["**/*.ts"]},null,2)}
|
|
583
|
-
`),await e(o(a,"toolkit.config.ts"),`import { auth } from '@murumets-ee/auth/plugin'
|
|
1691
|
+
`)}async function h(e,t){let{name:r}=t,i=a(e,`packages/config`);await n(a(i,`package.json`),`${JSON.stringify({name:`@${r}/config`,version:`0.1.0`,private:!0,type:`module`,exports:{".":`./toolkit.config.ts`,"./entities":`./entities/index.ts`,"./entities/*":`./entities/*`,"./app":`./lib/app.ts`,"./auth":`./lib/auth.ts`,"./auth-client":`./lib/auth-client.ts`},dependencies:{"@murumets-ee/core":`^${f.myorgCore}`,"@murumets-ee/db":`^${f.myorgDb}`,"@murumets-ee/entity":`^${f.myorgEntity}`,"@murumets-ee/logging":`^${f.myorgLogging}`,"@murumets-ee/auth":`^${f.myorgAuth}`,"better-auth":`^${f.betterAuth}`,"drizzle-orm":`^${f.drizzleOrm}`,postgres:`^${f.postgres}`,zod:`^${f.zod}`}},null,2)}\n`),await n(a(i,`tsconfig.json`),`${JSON.stringify({compilerOptions:{target:`ES2022`,module:`ESNext`,moduleResolution:`Bundler`,strict:!0,esModuleInterop:!0,skipLibCheck:!0,resolveJsonModule:!0},include:[`**/*.ts`]},null,2)}\n`),await n(a(i,`toolkit.config.ts`),`import { auth } from '@murumets-ee/auth/plugin'
|
|
584
1692
|
import { content } from '@murumets-ee/content/plugin'
|
|
585
1693
|
import { defineConfig } from '@murumets-ee/core'
|
|
586
1694
|
import { logging } from '@murumets-ee/logging/plugin'
|
|
@@ -634,7 +1742,7 @@ export default defineConfig({
|
|
|
634
1742
|
],
|
|
635
1743
|
projectRoot: import.meta.dirname,
|
|
636
1744
|
})
|
|
637
|
-
`),await
|
|
1745
|
+
`),await n(a(i,`auth.config.ts`),`import { betterAuth } from 'better-auth'
|
|
638
1746
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
|
639
1747
|
import { admin } from 'better-auth/plugins'
|
|
640
1748
|
import { organization } from 'better-auth/plugins/organization'
|
|
@@ -652,7 +1760,7 @@ export const auth = betterAuth({
|
|
|
652
1760
|
organization(),
|
|
653
1761
|
],
|
|
654
1762
|
})
|
|
655
|
-
`),await
|
|
1763
|
+
`),await n(a(i,`drizzle.config.ts`),`import type { Config } from 'drizzle-kit'
|
|
656
1764
|
|
|
657
1765
|
if (!process.env.DATABASE_URL) {
|
|
658
1766
|
throw new Error('DATABASE_URL environment variable is required')
|
|
@@ -666,9 +1774,9 @@ export default {
|
|
|
666
1774
|
url: process.env.DATABASE_URL,
|
|
667
1775
|
},
|
|
668
1776
|
} satisfies Config
|
|
669
|
-
`),await
|
|
1777
|
+
`),await n(a(i,`entities/index.ts`),`export { Article } from './article'
|
|
670
1778
|
export { Category } from './category'
|
|
671
|
-
`),await
|
|
1779
|
+
`),await n(a(i,`entities/article.ts`),`import { behavior, defineEntity, field } from '@murumets-ee/entity'
|
|
672
1780
|
|
|
673
1781
|
export const Article = defineEntity({
|
|
674
1782
|
name: 'article',
|
|
@@ -699,7 +1807,7 @@ export const Article = defineEntity({
|
|
|
699
1807
|
delete: 'group.admin',
|
|
700
1808
|
},
|
|
701
1809
|
})
|
|
702
|
-
`),await
|
|
1810
|
+
`),await n(a(i,`entities/category.ts`),`import { defineEntity, field } from '@murumets-ee/entity'
|
|
703
1811
|
|
|
704
1812
|
export const Category = defineEntity({
|
|
705
1813
|
name: 'category',
|
|
@@ -716,34 +1824,32 @@ export const Category = defineEntity({
|
|
|
716
1824
|
delete: 'group.admin',
|
|
717
1825
|
},
|
|
718
1826
|
})
|
|
719
|
-
`),await
|
|
1827
|
+
`),await n(a(i,`lib/app.ts`),`import { createApp, setApp, type ToolkitApp } from '@murumets-ee/core'
|
|
720
1828
|
import config from '../toolkit.config'
|
|
721
1829
|
|
|
722
1830
|
let appInstance: ToolkitApp | null = null
|
|
723
1831
|
|
|
724
1832
|
export async function getToolkitApp(): Promise<ToolkitApp> {
|
|
725
1833
|
if (!appInstance) {
|
|
726
|
-
console.log('Initializing toolkit app...')
|
|
727
1834
|
appInstance = await createApp(config)
|
|
728
1835
|
setApp(appInstance)
|
|
729
|
-
console.log('Toolkit app initialized')
|
|
730
1836
|
}
|
|
731
1837
|
return appInstance
|
|
732
1838
|
}
|
|
733
|
-
`),await
|
|
1839
|
+
`),await n(a(i,`lib/auth.ts`),`import { getToolkitApp } from './app'
|
|
734
1840
|
import { getAuth } from '@murumets-ee/auth'
|
|
735
1841
|
|
|
736
1842
|
export async function getAuthInstance() {
|
|
737
1843
|
await getToolkitApp()
|
|
738
1844
|
return getAuth()
|
|
739
1845
|
}
|
|
740
|
-
`),await
|
|
1846
|
+
`),await n(a(i,`lib/auth-client.ts`),`import { createClient } from '@murumets-ee/auth/client'
|
|
741
1847
|
|
|
742
1848
|
export const authClient = createClient()
|
|
743
|
-
`),await
|
|
1849
|
+
`),await n(a(i,`generated/auth-schema.ts`),`// This file is generated by better-auth CLI.
|
|
744
1850
|
// Run: npx @better-auth/cli generate --config auth.config.ts -y
|
|
745
1851
|
export {}
|
|
746
|
-
`),await
|
|
1852
|
+
`),await n(a(i,`scripts/generate-schema.ts`),`/**
|
|
747
1853
|
* Generate Drizzle schemas from entity definitions
|
|
748
1854
|
*/
|
|
749
1855
|
|
|
@@ -790,7 +1896,7 @@ async function generateSchemas() {
|
|
|
790
1896
|
}
|
|
791
1897
|
|
|
792
1898
|
generateSchemas().catch(console.error)
|
|
793
|
-
`),await
|
|
1899
|
+
`),await n(a(i,`scripts/migrate.ts`),`/**
|
|
794
1900
|
* Run pending migrations
|
|
795
1901
|
*/
|
|
796
1902
|
|
|
@@ -816,7 +1922,7 @@ async function migrate() {
|
|
|
816
1922
|
}
|
|
817
1923
|
|
|
818
1924
|
migrate()
|
|
819
|
-
`),await
|
|
1925
|
+
`),await n(a(i,`scripts/reset-db.ts`),`/**
|
|
820
1926
|
* Reset database (DROP all tables)
|
|
821
1927
|
* WARNING: Only for development
|
|
822
1928
|
*/
|
|
@@ -849,11 +1955,11 @@ async function resetDb() {
|
|
|
849
1955
|
}
|
|
850
1956
|
|
|
851
1957
|
resetDb().catch(console.error)
|
|
852
|
-
`)}async function
|
|
1958
|
+
`)}async function g(i,s){let{name:c}=s,l=a(i,`apps/admin`);await o(a(i,`apps`),{recursive:!0}),e(`pnpm create next-app@16 ${l} --typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias "@/*"`),await t(l,[`app/page.tsx`,`app/page.module.css`,`app/fonts`,`README.md`,`pnpm-workspace.yaml`]),await r(a(l,`package.json`),{dependencies:{[`@${c}/config`]:`workspace:*`,"@murumets-ee/auth-ui":`^${f.myorgAuthUi}`,"better-auth":`^${f.betterAuth}`,"next-intl":`^${f.nextIntl}`,"next-themes":`^${f.nextThemes}`,"lucide-react":`^${f.lucideReact}`,"react-hook-form":`^${f.reactHookForm}`,"@hookform/resolvers":`^${f.hookformResolvers}`,zod:`^${f.zod}`},devDependencies:{"babel-plugin-react-compiler":f.babelReactCompiler}});let{appendToFile:u}=await import(`./utils-Bmw75lEa.mjs`).then(e=>e.a);await u(a(l,`.gitignore`),`
|
|
853
1959
|
# Environment
|
|
854
1960
|
.env*
|
|
855
1961
|
!.env.example
|
|
856
|
-
`),await
|
|
1962
|
+
`),await n(a(l,`next.config.ts`),`import type { NextConfig } from 'next'
|
|
857
1963
|
import createNextIntlPlugin from 'next-intl/plugin'
|
|
858
1964
|
|
|
859
1965
|
const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
|
|
@@ -861,7 +1967,7 @@ const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
|
|
|
861
1967
|
const nextConfig: NextConfig = {
|
|
862
1968
|
reactCompiler: true,
|
|
863
1969
|
transpilePackages: [
|
|
864
|
-
'@${
|
|
1970
|
+
'@${c}/config',
|
|
865
1971
|
'@murumets-ee/core',
|
|
866
1972
|
'@murumets-ee/entity',
|
|
867
1973
|
'@murumets-ee/db',
|
|
@@ -877,7 +1983,8 @@ const nextConfig: NextConfig = {
|
|
|
877
1983
|
}
|
|
878
1984
|
|
|
879
1985
|
export default withNextIntl(nextConfig)
|
|
880
|
-
`),await
|
|
1986
|
+
`),await n(a(l,`proxy.ts`),`import { NextRequest, NextResponse } from 'next/server'
|
|
1987
|
+
import { getSessionCookie } from 'better-auth/cookies'
|
|
881
1988
|
import createMiddleware from 'next-intl/middleware'
|
|
882
1989
|
import { routing } from './i18n/routing'
|
|
883
1990
|
|
|
@@ -892,7 +1999,7 @@ export function proxy(request: NextRequest) {
|
|
|
892
1999
|
const pathWithoutLocale = pathname.replace(localePattern, '') || '/'
|
|
893
2000
|
|
|
894
2001
|
if (protectedPaths.some((p) => pathWithoutLocale.startsWith(p))) {
|
|
895
|
-
const session = request
|
|
2002
|
+
const session = getSessionCookie(request)
|
|
896
2003
|
if (!session) {
|
|
897
2004
|
const locale = pathname.match(localePattern)?.[1] || routing.defaultLocale
|
|
898
2005
|
return NextResponse.redirect(new URL(\`/\${locale}/auth/sign-in\`, request.url))
|
|
@@ -905,13 +2012,13 @@ export function proxy(request: NextRequest) {
|
|
|
905
2012
|
export const config = {
|
|
906
2013
|
matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)',
|
|
907
2014
|
}
|
|
908
|
-
`),await
|
|
2015
|
+
`),await n(a(l,`i18n/routing.ts`),`import { defineRouting } from 'next-intl/routing'
|
|
909
2016
|
|
|
910
2017
|
export const routing = defineRouting({
|
|
911
2018
|
locales: ['en'],
|
|
912
2019
|
defaultLocale: 'en',
|
|
913
2020
|
})
|
|
914
|
-
`),await
|
|
2021
|
+
`),await n(a(l,`i18n/request.ts`),`import { getRequestConfig } from 'next-intl/server'
|
|
915
2022
|
import { hasLocale } from 'next-intl'
|
|
916
2023
|
import { routing } from './routing'
|
|
917
2024
|
import { getAuthMessages } from '@murumets-ee/auth-ui/i18n'
|
|
@@ -931,7 +2038,7 @@ export default getRequestConfig(async ({ requestLocale }) => {
|
|
|
931
2038
|
},
|
|
932
2039
|
}
|
|
933
2040
|
})
|
|
934
|
-
`),await
|
|
2041
|
+
`),await n(a(l,`app/globals.css`),`@import "tailwindcss";
|
|
935
2042
|
@source "../node_modules/@murumets-ee/auth-ui/dist";
|
|
936
2043
|
|
|
937
2044
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
@@ -958,7 +2065,7 @@ body {
|
|
|
958
2065
|
color: var(--foreground);
|
|
959
2066
|
font-family: Arial, Helvetica, sans-serif;
|
|
960
2067
|
}
|
|
961
|
-
`),await
|
|
2068
|
+
`),await n(a(l,`app/theme-provider.tsx`),`'use client'
|
|
962
2069
|
|
|
963
2070
|
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
|
964
2071
|
import type { ReactNode } from 'react'
|
|
@@ -975,7 +2082,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
|
975
2082
|
</NextThemesProvider>
|
|
976
2083
|
)
|
|
977
2084
|
}
|
|
978
|
-
`),await
|
|
2085
|
+
`),await n(a(l,`app/theme-toggle.tsx`),`'use client'
|
|
979
2086
|
|
|
980
2087
|
import { useTheme } from 'next-themes'
|
|
981
2088
|
import { useState, useEffect } from 'react'
|
|
@@ -1002,7 +2109,7 @@ export function ThemeToggle() {
|
|
|
1002
2109
|
</button>
|
|
1003
2110
|
)
|
|
1004
2111
|
}
|
|
1005
|
-
`),await
|
|
2112
|
+
`),await n(a(l,`app/nav-header.tsx`),`'use client'
|
|
1006
2113
|
|
|
1007
2114
|
import Link from 'next/link'
|
|
1008
2115
|
import { usePathname } from 'next/navigation'
|
|
@@ -1032,7 +2139,7 @@ export function NavHeader() {
|
|
|
1032
2139
|
<header className="sticky top-0 z-50 backdrop-blur-md border-b bg-white/80 border-zinc-200 dark:bg-zinc-950/80 dark:border-zinc-800">
|
|
1033
2140
|
<nav className="max-w-6xl mx-auto flex items-center gap-1 px-4 h-12">
|
|
1034
2141
|
<span className="font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none">
|
|
1035
|
-
${
|
|
2142
|
+
${c} Admin
|
|
1036
2143
|
</span>
|
|
1037
2144
|
{links.map(({ href, label }) => (
|
|
1038
2145
|
<Link key={href} href={href} className={linkClass(href)}>
|
|
@@ -1046,12 +2153,12 @@ export function NavHeader() {
|
|
|
1046
2153
|
</header>
|
|
1047
2154
|
)
|
|
1048
2155
|
}
|
|
1049
|
-
`),await
|
|
2156
|
+
`),await n(a(l,`app/layout.tsx`),`import './globals.css'
|
|
1050
2157
|
|
|
1051
2158
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
1052
2159
|
return children
|
|
1053
2160
|
}
|
|
1054
|
-
`),await
|
|
2161
|
+
`),await n(a(l,`app/[locale]/layout.tsx`),`import type { Metadata } from 'next'
|
|
1055
2162
|
import { Geist, Geist_Mono } from 'next/font/google'
|
|
1056
2163
|
import { NextIntlClientProvider } from 'next-intl'
|
|
1057
2164
|
import { getMessages } from 'next-intl/server'
|
|
@@ -1071,7 +2178,7 @@ const geistMono = Geist_Mono({
|
|
|
1071
2178
|
})
|
|
1072
2179
|
|
|
1073
2180
|
export const metadata: Metadata = {
|
|
1074
|
-
title: '${
|
|
2181
|
+
title: '${c} Admin',
|
|
1075
2182
|
description: 'Built with Lumi CMS Toolkit',
|
|
1076
2183
|
}
|
|
1077
2184
|
|
|
@@ -1100,9 +2207,9 @@ export default async function LocaleLayout({
|
|
|
1100
2207
|
</html>
|
|
1101
2208
|
)
|
|
1102
2209
|
}
|
|
1103
|
-
`),await
|
|
1104
|
-
import { Article, Category } from '@${
|
|
1105
|
-
import { getToolkitApp } from '@${
|
|
2210
|
+
`),await n(a(l,`app/[locale]/page.tsx`),`import { createQueryClient } from '@murumets-ee/core/clients'
|
|
2211
|
+
import { Article, Category } from '@${c}/config/entities'
|
|
2212
|
+
import { getToolkitApp } from '@${c}/config/app'
|
|
1106
2213
|
|
|
1107
2214
|
export default async function HomePage() {
|
|
1108
2215
|
await getToolkitApp()
|
|
@@ -1117,7 +2224,7 @@ export default async function HomePage() {
|
|
|
1117
2224
|
return (
|
|
1118
2225
|
<div className="min-h-screen p-8">
|
|
1119
2226
|
<div className="max-w-4xl mx-auto">
|
|
1120
|
-
<h1 className="text-4xl font-bold mb-8">Welcome to ${
|
|
2227
|
+
<h1 className="text-4xl font-bold mb-8">Welcome to ${c}</h1>
|
|
1121
2228
|
|
|
1122
2229
|
<section className="mb-12">
|
|
1123
2230
|
<h2 className="text-2xl font-semibold mb-4">
|
|
@@ -1164,8 +2271,8 @@ export default async function HomePage() {
|
|
|
1164
2271
|
</div>
|
|
1165
2272
|
)
|
|
1166
2273
|
}
|
|
1167
|
-
`),await
|
|
1168
|
-
import { getAuthInstance } from '@${
|
|
2274
|
+
`),await n(a(l,`app/api/auth/[...all]/route.ts`),`import { toNextJsHandler } from 'better-auth/next-js'
|
|
2275
|
+
import { getAuthInstance } from '@${c}/config/auth'
|
|
1169
2276
|
|
|
1170
2277
|
let _handler: ReturnType<typeof toNextJsHandler> | null = null
|
|
1171
2278
|
|
|
@@ -1184,7 +2291,7 @@ export async function GET(req: Request) {
|
|
|
1184
2291
|
export async function POST(req: Request) {
|
|
1185
2292
|
return (await handler()).POST(req)
|
|
1186
2293
|
}
|
|
1187
|
-
`),await
|
|
2294
|
+
`),await n(a(l,`app/[locale]/auth/layout.tsx`),`import { AuthProviders } from './providers'
|
|
1188
2295
|
import type { ReactNode } from 'react'
|
|
1189
2296
|
|
|
1190
2297
|
export default function AuthLayout({ children }: { children: ReactNode }) {
|
|
@@ -1196,10 +2303,10 @@ export default function AuthLayout({ children }: { children: ReactNode }) {
|
|
|
1196
2303
|
</AuthProviders>
|
|
1197
2304
|
)
|
|
1198
2305
|
}
|
|
1199
|
-
`),await
|
|
2306
|
+
`),await n(a(l,`app/[locale]/auth/providers.tsx`),`'use client'
|
|
1200
2307
|
|
|
1201
2308
|
import { AuthUIProvider } from '@murumets-ee/auth-ui'
|
|
1202
|
-
import { authClient } from '@${
|
|
2309
|
+
import { authClient } from '@${c}/config/auth-client'
|
|
1203
2310
|
import Link from 'next/link'
|
|
1204
2311
|
import { useRouter } from 'next/navigation'
|
|
1205
2312
|
import { useLocale } from 'next-intl'
|
|
@@ -1222,22 +2329,22 @@ export function AuthProviders({ children }: { children: ReactNode }) {
|
|
|
1222
2329
|
</AuthUIProvider>
|
|
1223
2330
|
)
|
|
1224
2331
|
}
|
|
1225
|
-
`),await
|
|
2332
|
+
`),await n(a(l,`app/[locale]/auth/sign-in/page.tsx`),`import { SignInForm } from '@murumets-ee/auth-ui'
|
|
1226
2333
|
|
|
1227
2334
|
export default function SignInPage() {
|
|
1228
2335
|
return <SignInForm />
|
|
1229
2336
|
}
|
|
1230
|
-
`),await
|
|
2337
|
+
`),await n(a(l,`app/[locale]/auth/sign-up/page.tsx`),`import { SignUpForm } from '@murumets-ee/auth-ui'
|
|
1231
2338
|
|
|
1232
2339
|
export default function SignUpPage() {
|
|
1233
2340
|
return <SignUpForm />
|
|
1234
2341
|
}
|
|
1235
|
-
`),await
|
|
2342
|
+
`),await n(a(l,`app/[locale]/auth/forgot-password/page.tsx`),`import { ForgotPasswordForm } from '@murumets-ee/auth-ui'
|
|
1236
2343
|
|
|
1237
2344
|
export default function ForgotPasswordPage() {
|
|
1238
2345
|
return <ForgotPasswordForm />
|
|
1239
2346
|
}
|
|
1240
|
-
`),await
|
|
2347
|
+
`),await n(a(l,`app/[locale]/auth/reset-password/page.tsx`),`import { Suspense } from 'react'
|
|
1241
2348
|
import { ResetPasswordContent } from './content'
|
|
1242
2349
|
|
|
1243
2350
|
export default function ResetPasswordPage() {
|
|
@@ -1247,7 +2354,7 @@ export default function ResetPasswordPage() {
|
|
|
1247
2354
|
</Suspense>
|
|
1248
2355
|
)
|
|
1249
2356
|
}
|
|
1250
|
-
`),await
|
|
2357
|
+
`),await n(a(l,`app/[locale]/auth/reset-password/content.tsx`),`'use client'
|
|
1251
2358
|
|
|
1252
2359
|
import { useSearchParams } from 'next/navigation'
|
|
1253
2360
|
import { ResetPasswordForm } from '@murumets-ee/auth-ui'
|
|
@@ -1271,9 +2378,9 @@ export function ResetPasswordContent() {
|
|
|
1271
2378
|
|
|
1272
2379
|
return <ResetPasswordForm token={token} />
|
|
1273
2380
|
}
|
|
1274
|
-
`),await
|
|
2381
|
+
`),await n(a(l,`app/[locale]/setup/page.tsx`),`import { redirect } from 'next/navigation'
|
|
1275
2382
|
import { sql } from 'drizzle-orm'
|
|
1276
|
-
import { getToolkitApp } from '@${
|
|
2383
|
+
import { getToolkitApp } from '@${c}/config/app'
|
|
1277
2384
|
import { SetupForm } from './form'
|
|
1278
2385
|
|
|
1279
2386
|
export default async function SetupPage() {
|
|
@@ -1296,7 +2403,7 @@ export default async function SetupPage() {
|
|
|
1296
2403
|
</div>
|
|
1297
2404
|
)
|
|
1298
2405
|
}
|
|
1299
|
-
`),await
|
|
2406
|
+
`),await n(a(l,`app/[locale]/setup/form.tsx`),`'use client'
|
|
1300
2407
|
|
|
1301
2408
|
import { useState } from 'react'
|
|
1302
2409
|
import { createFirstAdmin } from './actions'
|
|
@@ -1367,10 +2474,10 @@ export function SetupForm() {
|
|
|
1367
2474
|
</form>
|
|
1368
2475
|
)
|
|
1369
2476
|
}
|
|
1370
|
-
`),await
|
|
2477
|
+
`),await n(a(l,`app/[locale]/setup/actions.ts`),`'use server'
|
|
1371
2478
|
|
|
1372
2479
|
import { sql } from 'drizzle-orm'
|
|
1373
|
-
import { getToolkitApp } from '@${
|
|
2480
|
+
import { getToolkitApp } from '@${c}/config/app'
|
|
1374
2481
|
import { getAuth } from '@murumets-ee/auth'
|
|
1375
2482
|
|
|
1376
2483
|
export async function createFirstAdmin(formData: FormData) {
|
|
@@ -1413,11 +2520,11 @@ export async function createFirstAdmin(formData: FormData) {
|
|
|
1413
2520
|
|
|
1414
2521
|
return { ok: true, email: adminUser.user.email }
|
|
1415
2522
|
}
|
|
1416
|
-
`)}async function
|
|
2523
|
+
`)}async function _(i,o){let{name:s}=o,c=a(i,`apps/web`);e(`pnpm create next-app@16 ${c} --typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias "@/*"`),await t(c,[`app/page.tsx`,`app/page.module.css`,`app/fonts`,`README.md`,`pnpm-workspace.yaml`]),await r(a(c,`package.json`),{dependencies:{[`@${s}/config`]:`workspace:*`,"@murumets-ee/core":`^${f.myorgCore}`,"@murumets-ee/auth-ui":`^${f.myorgAuthUi}`,"next-intl":`^${f.nextIntl}`,"next-themes":`^${f.nextThemes}`,"lucide-react":`^${f.lucideReact}`},devDependencies:{"babel-plugin-react-compiler":f.babelReactCompiler}});let{appendToFile:l}=await import(`./utils-Bmw75lEa.mjs`).then(e=>e.a);await l(a(c,`.gitignore`),`
|
|
1417
2524
|
# Environment
|
|
1418
2525
|
.env*
|
|
1419
2526
|
!.env.example
|
|
1420
|
-
`),await
|
|
2527
|
+
`),await n(a(c,`next.config.ts`),`import type { NextConfig } from 'next'
|
|
1421
2528
|
import createNextIntlPlugin from 'next-intl/plugin'
|
|
1422
2529
|
|
|
1423
2530
|
const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
|
|
@@ -1425,7 +2532,7 @@ const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
|
|
|
1425
2532
|
const nextConfig: NextConfig = {
|
|
1426
2533
|
reactCompiler: true,
|
|
1427
2534
|
transpilePackages: [
|
|
1428
|
-
'@${
|
|
2535
|
+
'@${s}/config',
|
|
1429
2536
|
'@murumets-ee/core',
|
|
1430
2537
|
'@murumets-ee/entity',
|
|
1431
2538
|
'@murumets-ee/db',
|
|
@@ -1436,7 +2543,7 @@ const nextConfig: NextConfig = {
|
|
|
1436
2543
|
}
|
|
1437
2544
|
|
|
1438
2545
|
export default withNextIntl(nextConfig)
|
|
1439
|
-
`),await
|
|
2546
|
+
`),await n(a(c,`proxy.ts`),`import createMiddleware from 'next-intl/middleware'
|
|
1440
2547
|
import { routing } from './i18n/routing'
|
|
1441
2548
|
|
|
1442
2549
|
export default createMiddleware(routing)
|
|
@@ -1444,13 +2551,13 @@ export default createMiddleware(routing)
|
|
|
1444
2551
|
export const config = {
|
|
1445
2552
|
matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)',
|
|
1446
2553
|
}
|
|
1447
|
-
`),await
|
|
2554
|
+
`),await n(a(c,`i18n/routing.ts`),`import { defineRouting } from 'next-intl/routing'
|
|
1448
2555
|
|
|
1449
2556
|
export const routing = defineRouting({
|
|
1450
2557
|
locales: ['en'],
|
|
1451
2558
|
defaultLocale: 'en',
|
|
1452
2559
|
})
|
|
1453
|
-
`),await
|
|
2560
|
+
`),await n(a(c,`i18n/request.ts`),`import { getRequestConfig } from 'next-intl/server'
|
|
1454
2561
|
import { hasLocale } from 'next-intl'
|
|
1455
2562
|
import { routing } from './routing'
|
|
1456
2563
|
import { getAuthMessages } from '@murumets-ee/auth-ui/i18n'
|
|
@@ -1470,7 +2577,7 @@ export default getRequestConfig(async ({ requestLocale }) => {
|
|
|
1470
2577
|
},
|
|
1471
2578
|
}
|
|
1472
2579
|
})
|
|
1473
|
-
`),await
|
|
2580
|
+
`),await n(a(c,`app/globals.css`),`@import "tailwindcss";
|
|
1474
2581
|
@source "../node_modules/@murumets-ee/auth-ui/dist";
|
|
1475
2582
|
|
|
1476
2583
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
@@ -1497,7 +2604,7 @@ body {
|
|
|
1497
2604
|
color: var(--foreground);
|
|
1498
2605
|
font-family: Arial, Helvetica, sans-serif;
|
|
1499
2606
|
}
|
|
1500
|
-
`),await
|
|
2607
|
+
`),await n(a(c,`app/theme-provider.tsx`),`'use client'
|
|
1501
2608
|
|
|
1502
2609
|
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
|
1503
2610
|
import type { ReactNode } from 'react'
|
|
@@ -1514,7 +2621,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
|
1514
2621
|
</NextThemesProvider>
|
|
1515
2622
|
)
|
|
1516
2623
|
}
|
|
1517
|
-
`),await
|
|
2624
|
+
`),await n(a(c,`app/theme-toggle.tsx`),`'use client'
|
|
1518
2625
|
|
|
1519
2626
|
import { useTheme } from 'next-themes'
|
|
1520
2627
|
import { useState, useEffect } from 'react'
|
|
@@ -1541,7 +2648,7 @@ export function ThemeToggle() {
|
|
|
1541
2648
|
</button>
|
|
1542
2649
|
)
|
|
1543
2650
|
}
|
|
1544
|
-
`),await
|
|
2651
|
+
`),await n(a(c,`app/nav-header.tsx`),`'use client'
|
|
1545
2652
|
|
|
1546
2653
|
import Link from 'next/link'
|
|
1547
2654
|
import { usePathname } from 'next/navigation'
|
|
@@ -1569,7 +2676,7 @@ export function NavHeader() {
|
|
|
1569
2676
|
<header className="sticky top-0 z-50 backdrop-blur-md border-b bg-white/80 border-zinc-200 dark:bg-zinc-950/80 dark:border-zinc-800">
|
|
1570
2677
|
<nav className="max-w-6xl mx-auto flex items-center gap-1 px-4 h-12">
|
|
1571
2678
|
<span className="font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none">
|
|
1572
|
-
${
|
|
2679
|
+
${s}
|
|
1573
2680
|
</span>
|
|
1574
2681
|
{links.map(({ href, label }) => (
|
|
1575
2682
|
<Link key={href} href={href} className={linkClass(href)}>
|
|
@@ -1583,12 +2690,12 @@ export function NavHeader() {
|
|
|
1583
2690
|
</header>
|
|
1584
2691
|
)
|
|
1585
2692
|
}
|
|
1586
|
-
`),await
|
|
2693
|
+
`),await n(a(c,`app/layout.tsx`),`import './globals.css'
|
|
1587
2694
|
|
|
1588
2695
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
1589
2696
|
return children
|
|
1590
2697
|
}
|
|
1591
|
-
`),await
|
|
2698
|
+
`),await n(a(c,`app/[locale]/layout.tsx`),`import type { Metadata } from 'next'
|
|
1592
2699
|
import { Geist, Geist_Mono } from 'next/font/google'
|
|
1593
2700
|
import { NextIntlClientProvider } from 'next-intl'
|
|
1594
2701
|
import { getMessages } from 'next-intl/server'
|
|
@@ -1608,7 +2715,7 @@ const geistMono = Geist_Mono({
|
|
|
1608
2715
|
})
|
|
1609
2716
|
|
|
1610
2717
|
export const metadata: Metadata = {
|
|
1611
|
-
title: '${
|
|
2718
|
+
title: '${s}',
|
|
1612
2719
|
description: 'Built with Lumi CMS Toolkit',
|
|
1613
2720
|
}
|
|
1614
2721
|
|
|
@@ -1637,9 +2744,9 @@ export default async function LocaleLayout({
|
|
|
1637
2744
|
</html>
|
|
1638
2745
|
)
|
|
1639
2746
|
}
|
|
1640
|
-
`),await
|
|
1641
|
-
import { Article, Category } from '@${
|
|
1642
|
-
import { getToolkitApp } from '@${
|
|
2747
|
+
`),await n(a(c,`app/[locale]/page.tsx`),`import { createQueryClient } from '@murumets-ee/core/clients'
|
|
2748
|
+
import { Article, Category } from '@${s}/config/entities'
|
|
2749
|
+
import { getToolkitApp } from '@${s}/config/app'
|
|
1643
2750
|
|
|
1644
2751
|
export default async function HomePage() {
|
|
1645
2752
|
await getToolkitApp()
|
|
@@ -1654,7 +2761,7 @@ export default async function HomePage() {
|
|
|
1654
2761
|
return (
|
|
1655
2762
|
<div className="min-h-screen p-8">
|
|
1656
2763
|
<div className="max-w-4xl mx-auto">
|
|
1657
|
-
<h1 className="text-4xl font-bold mb-8">Welcome to ${
|
|
2764
|
+
<h1 className="text-4xl font-bold mb-8">Welcome to ${s}</h1>
|
|
1658
2765
|
|
|
1659
2766
|
<section className="mb-12">
|
|
1660
2767
|
<h2 className="text-2xl font-semibold mb-4">
|
|
@@ -1701,7 +2808,7 @@ export default async function HomePage() {
|
|
|
1701
2808
|
</div>
|
|
1702
2809
|
)
|
|
1703
2810
|
}
|
|
1704
|
-
`)}
|
|
2811
|
+
`)}async function v(e,t){let{name:r}=t;await n(a(e,`next.config.ts`),`import type { NextConfig } from 'next'
|
|
1705
2812
|
import createNextIntlPlugin from 'next-intl/plugin'
|
|
1706
2813
|
|
|
1707
2814
|
const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
|
|
@@ -1726,7 +2833,8 @@ const nextConfig: NextConfig = {
|
|
|
1726
2833
|
}
|
|
1727
2834
|
|
|
1728
2835
|
export default withNextIntl(nextConfig)
|
|
1729
|
-
`),await
|
|
2836
|
+
`),await n(a(e,`proxy.ts`),`import { NextRequest, NextResponse } from 'next/server'
|
|
2837
|
+
import { getSessionCookie } from 'better-auth/cookies'
|
|
1730
2838
|
import createMiddleware from 'next-intl/middleware'
|
|
1731
2839
|
import { routing } from './i18n/routing'
|
|
1732
2840
|
|
|
@@ -1743,7 +2851,7 @@ export function proxy(request: NextRequest) {
|
|
|
1743
2851
|
|
|
1744
2852
|
// Auth check for protected paths
|
|
1745
2853
|
if (protectedPaths.some((p) => pathWithoutLocale.startsWith(p))) {
|
|
1746
|
-
const session = request
|
|
2854
|
+
const session = getSessionCookie(request)
|
|
1747
2855
|
if (!session) {
|
|
1748
2856
|
const locale = pathname.match(localePattern)?.[1] || routing.defaultLocale
|
|
1749
2857
|
return NextResponse.redirect(new URL(\`/\${locale}/auth/sign-in\`, request.url))
|
|
@@ -1757,12 +2865,12 @@ export function proxy(request: NextRequest) {
|
|
|
1757
2865
|
export const config = {
|
|
1758
2866
|
matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)',
|
|
1759
2867
|
}
|
|
1760
|
-
`),await
|
|
2868
|
+
`),await n(a(e,`app/layout.tsx`),`import './globals.css'
|
|
1761
2869
|
|
|
1762
2870
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
1763
2871
|
return children
|
|
1764
2872
|
}
|
|
1765
|
-
`),await
|
|
2873
|
+
`),await n(a(e,`app/[locale]/layout.tsx`),`import type { Metadata } from 'next'
|
|
1766
2874
|
import { Geist, Geist_Mono } from 'next/font/google'
|
|
1767
2875
|
import { NextIntlClientProvider } from 'next-intl'
|
|
1768
2876
|
import { getMessages } from 'next-intl/server'
|
|
@@ -1811,7 +2919,7 @@ export default async function LocaleLayout({
|
|
|
1811
2919
|
</html>
|
|
1812
2920
|
)
|
|
1813
2921
|
}
|
|
1814
|
-
`),await
|
|
2922
|
+
`),await n(a(e,`app/[locale]/page.tsx`),`import { createQueryClient } from '@murumets-ee/core/clients'
|
|
1815
2923
|
import { Article, Category } from '@/entities'
|
|
1816
2924
|
import { getToolkitApp } from '@/lib/app'
|
|
1817
2925
|
|
|
@@ -1875,13 +2983,49 @@ export default async function HomePage() {
|
|
|
1875
2983
|
</div>
|
|
1876
2984
|
)
|
|
1877
2985
|
}
|
|
1878
|
-
`)}
|
|
2986
|
+
`)}async function y(e,t){await n(a(e,`scripts/generate-schema.ts`),`import { execSync } from 'node:child_process'
|
|
1879
2987
|
execSync('npx @murumets-ee/cli generate', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })
|
|
1880
|
-
`),await
|
|
2988
|
+
`),await n(a(e,`scripts/migrate.ts`),`import { execSync } from 'node:child_process'
|
|
1881
2989
|
execSync('npx @murumets-ee/cli migrate', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })
|
|
1882
|
-
`),await
|
|
2990
|
+
`),await n(a(e,`scripts/reset-db.ts`),`import { execSync } from 'node:child_process'
|
|
1883
2991
|
execSync('npx @murumets-ee/cli reset --force', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })
|
|
1884
|
-
`)
|
|
2992
|
+
`),await n(a(e,`scripts/worker.ts`),`/**
|
|
2993
|
+
* Standalone queue worker process.
|
|
2994
|
+
*
|
|
2995
|
+
* Run separately from the Next.js web server:
|
|
2996
|
+
* pnpm worker (or: tsx --env-file=.env scripts/worker.ts)
|
|
2997
|
+
*
|
|
2998
|
+
* The web server should set QUEUE_WORKER=false so it only enqueues jobs.
|
|
2999
|
+
* This process handles job execution.
|
|
3000
|
+
*/
|
|
3001
|
+
|
|
3002
|
+
import { createApp, setApp } from '@murumets-ee/core'
|
|
3003
|
+
import config from '../toolkit.config'
|
|
3004
|
+
|
|
3005
|
+
process.env.QUEUE_WORKER = 'true'
|
|
3006
|
+
|
|
3007
|
+
const app = await createApp(config)
|
|
3008
|
+
setApp(app)
|
|
3009
|
+
|
|
3010
|
+
app.logger.info('Queue worker process running. Press Ctrl+C to stop.')
|
|
3011
|
+
|
|
3012
|
+
const shutdown = async () => {
|
|
3013
|
+
app.logger.info('Shutting down queue worker...')
|
|
3014
|
+
|
|
3015
|
+
const worker = (globalThis as Record<symbol, unknown>)[
|
|
3016
|
+
Symbol.for('@murumets-ee/queue:worker')
|
|
3017
|
+
] as { stop: () => Promise<void> } | undefined
|
|
3018
|
+
|
|
3019
|
+
if (worker) {
|
|
3020
|
+
await worker.stop()
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
process.exit(0)
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
process.on('SIGINT', shutdown)
|
|
3027
|
+
process.on('SIGTERM', shutdown)
|
|
3028
|
+
`)}async function b(e,t){let{name:r}=t;await n(a(e,`app/globals.css`),`@import "tailwindcss";
|
|
1885
3029
|
@source "../node_modules/@murumets-ee/auth-ui/dist";
|
|
1886
3030
|
|
|
1887
3031
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
@@ -1908,7 +3052,7 @@ body {
|
|
|
1908
3052
|
color: var(--foreground);
|
|
1909
3053
|
font-family: Arial, Helvetica, sans-serif;
|
|
1910
3054
|
}
|
|
1911
|
-
`),await
|
|
3055
|
+
`),await n(a(e,`app/theme-provider.tsx`),`'use client'
|
|
1912
3056
|
|
|
1913
3057
|
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
|
1914
3058
|
import type { ReactNode } from 'react'
|
|
@@ -1925,7 +3069,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
|
1925
3069
|
</NextThemesProvider>
|
|
1926
3070
|
)
|
|
1927
3071
|
}
|
|
1928
|
-
`),await
|
|
3072
|
+
`),await n(a(e,`app/theme-toggle.tsx`),`'use client'
|
|
1929
3073
|
|
|
1930
3074
|
import { useTheme } from 'next-themes'
|
|
1931
3075
|
import { useState, useEffect } from 'react'
|
|
@@ -1952,7 +3096,7 @@ export function ThemeToggle() {
|
|
|
1952
3096
|
</button>
|
|
1953
3097
|
)
|
|
1954
3098
|
}
|
|
1955
|
-
`),await
|
|
3099
|
+
`),await n(a(e,`app/nav-header.tsx`),`'use client'
|
|
1956
3100
|
|
|
1957
3101
|
import Link from 'next/link'
|
|
1958
3102
|
import { usePathname } from 'next/navigation'
|
|
@@ -1996,7 +3140,7 @@ export function NavHeader() {
|
|
|
1996
3140
|
</header>
|
|
1997
3141
|
)
|
|
1998
3142
|
}
|
|
1999
|
-
`)}
|
|
3143
|
+
`)}async function x(e,t){let{name:r}=t;await n(a(e,`toolkit.config.ts`),`import { auth } from '@murumets-ee/auth/plugin'
|
|
2000
3144
|
import { content } from '@murumets-ee/content/plugin'
|
|
2001
3145
|
import { defineConfig } from '@murumets-ee/core'
|
|
2002
3146
|
import { logging } from '@murumets-ee/logging/plugin'
|
|
@@ -2050,21 +3194,19 @@ export default defineConfig({
|
|
|
2050
3194
|
],
|
|
2051
3195
|
projectRoot: import.meta.dirname,
|
|
2052
3196
|
})
|
|
2053
|
-
`),await
|
|
3197
|
+
`),await n(a(e,`lib/app.ts`),`import { createApp, setApp, type ToolkitApp } from '@murumets-ee/core'
|
|
2054
3198
|
import config from '../toolkit.config'
|
|
2055
3199
|
|
|
2056
3200
|
let appInstance: ToolkitApp | null = null
|
|
2057
3201
|
|
|
2058
3202
|
export async function getToolkitApp(): Promise<ToolkitApp> {
|
|
2059
3203
|
if (!appInstance) {
|
|
2060
|
-
console.log('Initializing toolkit app...')
|
|
2061
3204
|
appInstance = await createApp(config)
|
|
2062
3205
|
setApp(appInstance)
|
|
2063
|
-
console.log('Toolkit app initialized')
|
|
2064
3206
|
}
|
|
2065
3207
|
return appInstance
|
|
2066
3208
|
}
|
|
2067
|
-
`),await
|
|
3209
|
+
`),await n(a(e,`drizzle.config.ts`),`import type { Config } from 'drizzle-kit'
|
|
2068
3210
|
|
|
2069
3211
|
if (!process.env.DATABASE_URL) {
|
|
2070
3212
|
throw new Error('DATABASE_URL environment variable is required')
|
|
@@ -2078,7 +3220,7 @@ export default {
|
|
|
2078
3220
|
url: process.env.DATABASE_URL,
|
|
2079
3221
|
},
|
|
2080
3222
|
} satisfies Config
|
|
2081
|
-
`),await
|
|
3223
|
+
`),await n(a(e,`auth.config.ts`),`import { betterAuth } from 'better-auth'
|
|
2082
3224
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
|
2083
3225
|
import { admin } from 'better-auth/plugins'
|
|
2084
3226
|
import { organization } from 'better-auth/plugins/organization'
|
|
@@ -2096,7 +3238,8 @@ export const auth = betterAuth({
|
|
|
2096
3238
|
organization(),
|
|
2097
3239
|
],
|
|
2098
3240
|
})
|
|
2099
|
-
`),await
|
|
3241
|
+
`),await n(a(e,`generated/auth-schema.ts`),`// This file is generated by better-auth CLI.
|
|
2100
3242
|
// Run: npx @better-auth/cli generate --config auth.config.ts -y
|
|
2101
3243
|
export {}
|
|
2102
|
-
`)}async function
|
|
3244
|
+
`)}async function S(e,t){e.mode===`single`?await C(e,t):await w(e,t)}async function C(n,i){let o=a(process.cwd(),n.name);i?.(`Creating Next.js app...`),e(`pnpm create next-app@16 ${n.name} --typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias "@/*"`),await t(o,[`app/page.tsx`,`app/page.module.css`,`app/fonts`,`README.md`]),await r(a(o,`package.json`),{type:`module`,dependencies:{"@murumets-ee/core":`^${f.myorgCore}`,"@murumets-ee/db":`^${f.myorgDb}`,"@murumets-ee/entity":`^${f.myorgEntity}`,"@murumets-ee/logging":`^${f.myorgLogging}`,"@murumets-ee/auth":`^${f.myorgAuth}`,"@murumets-ee/auth-ui":`^${f.myorgAuthUi}`,"@murumets-ee/admin-ui":`^${f.myorgAdminUi}`,"@murumets-ee/content":`^${f.myorgContent}`,"@murumets-ee/settings":`^${f.myorgSettings}`,"@murumets-ee/storage":`^${f.myorgStorage}`,"@murumets-ee/media":`^${f.myorgMedia}`,"@murumets-ee/taxonomy":`^${f.myorgTaxonomy}`,"@murumets-ee/queue":`^${f.myorgQueue}`,"@murumets-ee/mail":`^${f.myorgMail}`,"@murumets-ee/ticketing":`^${f.myorgTicketing}`,"@murumets-ee/ticketing-ui":`^${f.myorgTicketingUi}`,"@murumets-ee/tokens":`^${f.myorgTokens}`,"@murumets-ee/editor":`^${f.myorgEditor}`,"@murumets-ee/blocks":`^${f.myorgBlocks}`,"@murumets-ee/ui":`^${f.myorgUi}`,"better-auth":`^${f.betterAuth}`,"drizzle-orm":`^${f.drizzleOrm}`,"next-intl":`^${f.nextIntl}`,"next-themes":`^${f.nextThemes}`,"lucide-react":`^${f.lucideReact}`,postgres:`^${f.postgres}`,"@tanstack/react-query":`^${f.tanstackReactQuery}`,"@tanstack/react-table":`^${f.tanstackReactTable}`,"react-hook-form":`^${f.reactHookForm}`,"@hookform/resolvers":`^${f.hookformResolvers}`,zod:`^${f.zod}`},devDependencies:{"drizzle-kit":`^${f.drizzleKit}`,tsx:`^${f.tsx}`,"babel-plugin-react-compiler":f.babelReactCompiler},scripts:{"db:generate":`tsx --env-file=.env scripts/generate-schema.ts`,"db:migrate:generate":`drizzle-kit generate`,"db:migrate":`tsx --env-file=.env scripts/migrate.ts`,"db:reset":`tsx --env-file=.env scripts/reset-db.ts`,worker:`tsx --env-file=.env scripts/worker.ts`}}),i?.(`Adding toolkit files...`),await l(o,n),await u(o,n),await x(o,n),await c(o,n),await d(o,n),await v(o,n),await b(o,n),await s(o,n),await y(o,n),n.installDeps&&(i?.(`Installing dependencies...`),e(`pnpm install`,{cwd:o}))}async function w(t,n){let r=a(process.cwd(),t.name);n?.(`Creating workspace...`),await p(r,t,n),t.installDeps&&(n?.(`Installing dependencies...`),e(`pnpm install`,{cwd:r}))}export{S as scaffold};
|
|
3245
|
+
//# sourceMappingURL=index.mjs.map
|