@murumets-ee/create 0.1.13 → 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} +1261 -120
- 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 -2106
- 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,7 @@ 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'
|
|
881
1987
|
import { getSessionCookie } from 'better-auth/cookies'
|
|
882
1988
|
import createMiddleware from 'next-intl/middleware'
|
|
883
1989
|
import { routing } from './i18n/routing'
|
|
@@ -906,13 +2012,13 @@ export function proxy(request: NextRequest) {
|
|
|
906
2012
|
export const config = {
|
|
907
2013
|
matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)',
|
|
908
2014
|
}
|
|
909
|
-
`),await
|
|
2015
|
+
`),await n(a(l,`i18n/routing.ts`),`import { defineRouting } from 'next-intl/routing'
|
|
910
2016
|
|
|
911
2017
|
export const routing = defineRouting({
|
|
912
2018
|
locales: ['en'],
|
|
913
2019
|
defaultLocale: 'en',
|
|
914
2020
|
})
|
|
915
|
-
`),await
|
|
2021
|
+
`),await n(a(l,`i18n/request.ts`),`import { getRequestConfig } from 'next-intl/server'
|
|
916
2022
|
import { hasLocale } from 'next-intl'
|
|
917
2023
|
import { routing } from './routing'
|
|
918
2024
|
import { getAuthMessages } from '@murumets-ee/auth-ui/i18n'
|
|
@@ -932,7 +2038,7 @@ export default getRequestConfig(async ({ requestLocale }) => {
|
|
|
932
2038
|
},
|
|
933
2039
|
}
|
|
934
2040
|
})
|
|
935
|
-
`),await
|
|
2041
|
+
`),await n(a(l,`app/globals.css`),`@import "tailwindcss";
|
|
936
2042
|
@source "../node_modules/@murumets-ee/auth-ui/dist";
|
|
937
2043
|
|
|
938
2044
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
@@ -959,7 +2065,7 @@ body {
|
|
|
959
2065
|
color: var(--foreground);
|
|
960
2066
|
font-family: Arial, Helvetica, sans-serif;
|
|
961
2067
|
}
|
|
962
|
-
`),await
|
|
2068
|
+
`),await n(a(l,`app/theme-provider.tsx`),`'use client'
|
|
963
2069
|
|
|
964
2070
|
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
|
965
2071
|
import type { ReactNode } from 'react'
|
|
@@ -976,7 +2082,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
|
976
2082
|
</NextThemesProvider>
|
|
977
2083
|
)
|
|
978
2084
|
}
|
|
979
|
-
`),await
|
|
2085
|
+
`),await n(a(l,`app/theme-toggle.tsx`),`'use client'
|
|
980
2086
|
|
|
981
2087
|
import { useTheme } from 'next-themes'
|
|
982
2088
|
import { useState, useEffect } from 'react'
|
|
@@ -1003,7 +2109,7 @@ export function ThemeToggle() {
|
|
|
1003
2109
|
</button>
|
|
1004
2110
|
)
|
|
1005
2111
|
}
|
|
1006
|
-
`),await
|
|
2112
|
+
`),await n(a(l,`app/nav-header.tsx`),`'use client'
|
|
1007
2113
|
|
|
1008
2114
|
import Link from 'next/link'
|
|
1009
2115
|
import { usePathname } from 'next/navigation'
|
|
@@ -1033,7 +2139,7 @@ export function NavHeader() {
|
|
|
1033
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">
|
|
1034
2140
|
<nav className="max-w-6xl mx-auto flex items-center gap-1 px-4 h-12">
|
|
1035
2141
|
<span className="font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none">
|
|
1036
|
-
${
|
|
2142
|
+
${c} Admin
|
|
1037
2143
|
</span>
|
|
1038
2144
|
{links.map(({ href, label }) => (
|
|
1039
2145
|
<Link key={href} href={href} className={linkClass(href)}>
|
|
@@ -1047,12 +2153,12 @@ export function NavHeader() {
|
|
|
1047
2153
|
</header>
|
|
1048
2154
|
)
|
|
1049
2155
|
}
|
|
1050
|
-
`),await
|
|
2156
|
+
`),await n(a(l,`app/layout.tsx`),`import './globals.css'
|
|
1051
2157
|
|
|
1052
2158
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
1053
2159
|
return children
|
|
1054
2160
|
}
|
|
1055
|
-
`),await
|
|
2161
|
+
`),await n(a(l,`app/[locale]/layout.tsx`),`import type { Metadata } from 'next'
|
|
1056
2162
|
import { Geist, Geist_Mono } from 'next/font/google'
|
|
1057
2163
|
import { NextIntlClientProvider } from 'next-intl'
|
|
1058
2164
|
import { getMessages } from 'next-intl/server'
|
|
@@ -1072,7 +2178,7 @@ const geistMono = Geist_Mono({
|
|
|
1072
2178
|
})
|
|
1073
2179
|
|
|
1074
2180
|
export const metadata: Metadata = {
|
|
1075
|
-
title: '${
|
|
2181
|
+
title: '${c} Admin',
|
|
1076
2182
|
description: 'Built with Lumi CMS Toolkit',
|
|
1077
2183
|
}
|
|
1078
2184
|
|
|
@@ -1101,9 +2207,9 @@ export default async function LocaleLayout({
|
|
|
1101
2207
|
</html>
|
|
1102
2208
|
)
|
|
1103
2209
|
}
|
|
1104
|
-
`),await
|
|
1105
|
-
import { Article, Category } from '@${
|
|
1106
|
-
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'
|
|
1107
2213
|
|
|
1108
2214
|
export default async function HomePage() {
|
|
1109
2215
|
await getToolkitApp()
|
|
@@ -1118,7 +2224,7 @@ export default async function HomePage() {
|
|
|
1118
2224
|
return (
|
|
1119
2225
|
<div className="min-h-screen p-8">
|
|
1120
2226
|
<div className="max-w-4xl mx-auto">
|
|
1121
|
-
<h1 className="text-4xl font-bold mb-8">Welcome to ${
|
|
2227
|
+
<h1 className="text-4xl font-bold mb-8">Welcome to ${c}</h1>
|
|
1122
2228
|
|
|
1123
2229
|
<section className="mb-12">
|
|
1124
2230
|
<h2 className="text-2xl font-semibold mb-4">
|
|
@@ -1165,8 +2271,8 @@ export default async function HomePage() {
|
|
|
1165
2271
|
</div>
|
|
1166
2272
|
)
|
|
1167
2273
|
}
|
|
1168
|
-
`),await
|
|
1169
|
-
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'
|
|
1170
2276
|
|
|
1171
2277
|
let _handler: ReturnType<typeof toNextJsHandler> | null = null
|
|
1172
2278
|
|
|
@@ -1185,7 +2291,7 @@ export async function GET(req: Request) {
|
|
|
1185
2291
|
export async function POST(req: Request) {
|
|
1186
2292
|
return (await handler()).POST(req)
|
|
1187
2293
|
}
|
|
1188
|
-
`),await
|
|
2294
|
+
`),await n(a(l,`app/[locale]/auth/layout.tsx`),`import { AuthProviders } from './providers'
|
|
1189
2295
|
import type { ReactNode } from 'react'
|
|
1190
2296
|
|
|
1191
2297
|
export default function AuthLayout({ children }: { children: ReactNode }) {
|
|
@@ -1197,10 +2303,10 @@ export default function AuthLayout({ children }: { children: ReactNode }) {
|
|
|
1197
2303
|
</AuthProviders>
|
|
1198
2304
|
)
|
|
1199
2305
|
}
|
|
1200
|
-
`),await
|
|
2306
|
+
`),await n(a(l,`app/[locale]/auth/providers.tsx`),`'use client'
|
|
1201
2307
|
|
|
1202
2308
|
import { AuthUIProvider } from '@murumets-ee/auth-ui'
|
|
1203
|
-
import { authClient } from '@${
|
|
2309
|
+
import { authClient } from '@${c}/config/auth-client'
|
|
1204
2310
|
import Link from 'next/link'
|
|
1205
2311
|
import { useRouter } from 'next/navigation'
|
|
1206
2312
|
import { useLocale } from 'next-intl'
|
|
@@ -1223,22 +2329,22 @@ export function AuthProviders({ children }: { children: ReactNode }) {
|
|
|
1223
2329
|
</AuthUIProvider>
|
|
1224
2330
|
)
|
|
1225
2331
|
}
|
|
1226
|
-
`),await
|
|
2332
|
+
`),await n(a(l,`app/[locale]/auth/sign-in/page.tsx`),`import { SignInForm } from '@murumets-ee/auth-ui'
|
|
1227
2333
|
|
|
1228
2334
|
export default function SignInPage() {
|
|
1229
2335
|
return <SignInForm />
|
|
1230
2336
|
}
|
|
1231
|
-
`),await
|
|
2337
|
+
`),await n(a(l,`app/[locale]/auth/sign-up/page.tsx`),`import { SignUpForm } from '@murumets-ee/auth-ui'
|
|
1232
2338
|
|
|
1233
2339
|
export default function SignUpPage() {
|
|
1234
2340
|
return <SignUpForm />
|
|
1235
2341
|
}
|
|
1236
|
-
`),await
|
|
2342
|
+
`),await n(a(l,`app/[locale]/auth/forgot-password/page.tsx`),`import { ForgotPasswordForm } from '@murumets-ee/auth-ui'
|
|
1237
2343
|
|
|
1238
2344
|
export default function ForgotPasswordPage() {
|
|
1239
2345
|
return <ForgotPasswordForm />
|
|
1240
2346
|
}
|
|
1241
|
-
`),await
|
|
2347
|
+
`),await n(a(l,`app/[locale]/auth/reset-password/page.tsx`),`import { Suspense } from 'react'
|
|
1242
2348
|
import { ResetPasswordContent } from './content'
|
|
1243
2349
|
|
|
1244
2350
|
export default function ResetPasswordPage() {
|
|
@@ -1248,7 +2354,7 @@ export default function ResetPasswordPage() {
|
|
|
1248
2354
|
</Suspense>
|
|
1249
2355
|
)
|
|
1250
2356
|
}
|
|
1251
|
-
`),await
|
|
2357
|
+
`),await n(a(l,`app/[locale]/auth/reset-password/content.tsx`),`'use client'
|
|
1252
2358
|
|
|
1253
2359
|
import { useSearchParams } from 'next/navigation'
|
|
1254
2360
|
import { ResetPasswordForm } from '@murumets-ee/auth-ui'
|
|
@@ -1272,9 +2378,9 @@ export function ResetPasswordContent() {
|
|
|
1272
2378
|
|
|
1273
2379
|
return <ResetPasswordForm token={token} />
|
|
1274
2380
|
}
|
|
1275
|
-
`),await
|
|
2381
|
+
`),await n(a(l,`app/[locale]/setup/page.tsx`),`import { redirect } from 'next/navigation'
|
|
1276
2382
|
import { sql } from 'drizzle-orm'
|
|
1277
|
-
import { getToolkitApp } from '@${
|
|
2383
|
+
import { getToolkitApp } from '@${c}/config/app'
|
|
1278
2384
|
import { SetupForm } from './form'
|
|
1279
2385
|
|
|
1280
2386
|
export default async function SetupPage() {
|
|
@@ -1297,7 +2403,7 @@ export default async function SetupPage() {
|
|
|
1297
2403
|
</div>
|
|
1298
2404
|
)
|
|
1299
2405
|
}
|
|
1300
|
-
`),await
|
|
2406
|
+
`),await n(a(l,`app/[locale]/setup/form.tsx`),`'use client'
|
|
1301
2407
|
|
|
1302
2408
|
import { useState } from 'react'
|
|
1303
2409
|
import { createFirstAdmin } from './actions'
|
|
@@ -1368,10 +2474,10 @@ export function SetupForm() {
|
|
|
1368
2474
|
</form>
|
|
1369
2475
|
)
|
|
1370
2476
|
}
|
|
1371
|
-
`),await
|
|
2477
|
+
`),await n(a(l,`app/[locale]/setup/actions.ts`),`'use server'
|
|
1372
2478
|
|
|
1373
2479
|
import { sql } from 'drizzle-orm'
|
|
1374
|
-
import { getToolkitApp } from '@${
|
|
2480
|
+
import { getToolkitApp } from '@${c}/config/app'
|
|
1375
2481
|
import { getAuth } from '@murumets-ee/auth'
|
|
1376
2482
|
|
|
1377
2483
|
export async function createFirstAdmin(formData: FormData) {
|
|
@@ -1414,11 +2520,11 @@ export async function createFirstAdmin(formData: FormData) {
|
|
|
1414
2520
|
|
|
1415
2521
|
return { ok: true, email: adminUser.user.email }
|
|
1416
2522
|
}
|
|
1417
|
-
`)}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`),`
|
|
1418
2524
|
# Environment
|
|
1419
2525
|
.env*
|
|
1420
2526
|
!.env.example
|
|
1421
|
-
`),await
|
|
2527
|
+
`),await n(a(c,`next.config.ts`),`import type { NextConfig } from 'next'
|
|
1422
2528
|
import createNextIntlPlugin from 'next-intl/plugin'
|
|
1423
2529
|
|
|
1424
2530
|
const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
|
|
@@ -1426,7 +2532,7 @@ const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
|
|
|
1426
2532
|
const nextConfig: NextConfig = {
|
|
1427
2533
|
reactCompiler: true,
|
|
1428
2534
|
transpilePackages: [
|
|
1429
|
-
'@${
|
|
2535
|
+
'@${s}/config',
|
|
1430
2536
|
'@murumets-ee/core',
|
|
1431
2537
|
'@murumets-ee/entity',
|
|
1432
2538
|
'@murumets-ee/db',
|
|
@@ -1437,7 +2543,7 @@ const nextConfig: NextConfig = {
|
|
|
1437
2543
|
}
|
|
1438
2544
|
|
|
1439
2545
|
export default withNextIntl(nextConfig)
|
|
1440
|
-
`),await
|
|
2546
|
+
`),await n(a(c,`proxy.ts`),`import createMiddleware from 'next-intl/middleware'
|
|
1441
2547
|
import { routing } from './i18n/routing'
|
|
1442
2548
|
|
|
1443
2549
|
export default createMiddleware(routing)
|
|
@@ -1445,13 +2551,13 @@ export default createMiddleware(routing)
|
|
|
1445
2551
|
export const config = {
|
|
1446
2552
|
matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)',
|
|
1447
2553
|
}
|
|
1448
|
-
`),await
|
|
2554
|
+
`),await n(a(c,`i18n/routing.ts`),`import { defineRouting } from 'next-intl/routing'
|
|
1449
2555
|
|
|
1450
2556
|
export const routing = defineRouting({
|
|
1451
2557
|
locales: ['en'],
|
|
1452
2558
|
defaultLocale: 'en',
|
|
1453
2559
|
})
|
|
1454
|
-
`),await
|
|
2560
|
+
`),await n(a(c,`i18n/request.ts`),`import { getRequestConfig } from 'next-intl/server'
|
|
1455
2561
|
import { hasLocale } from 'next-intl'
|
|
1456
2562
|
import { routing } from './routing'
|
|
1457
2563
|
import { getAuthMessages } from '@murumets-ee/auth-ui/i18n'
|
|
@@ -1471,7 +2577,7 @@ export default getRequestConfig(async ({ requestLocale }) => {
|
|
|
1471
2577
|
},
|
|
1472
2578
|
}
|
|
1473
2579
|
})
|
|
1474
|
-
`),await
|
|
2580
|
+
`),await n(a(c,`app/globals.css`),`@import "tailwindcss";
|
|
1475
2581
|
@source "../node_modules/@murumets-ee/auth-ui/dist";
|
|
1476
2582
|
|
|
1477
2583
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
@@ -1498,7 +2604,7 @@ body {
|
|
|
1498
2604
|
color: var(--foreground);
|
|
1499
2605
|
font-family: Arial, Helvetica, sans-serif;
|
|
1500
2606
|
}
|
|
1501
|
-
`),await
|
|
2607
|
+
`),await n(a(c,`app/theme-provider.tsx`),`'use client'
|
|
1502
2608
|
|
|
1503
2609
|
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
|
1504
2610
|
import type { ReactNode } from 'react'
|
|
@@ -1515,7 +2621,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
|
1515
2621
|
</NextThemesProvider>
|
|
1516
2622
|
)
|
|
1517
2623
|
}
|
|
1518
|
-
`),await
|
|
2624
|
+
`),await n(a(c,`app/theme-toggle.tsx`),`'use client'
|
|
1519
2625
|
|
|
1520
2626
|
import { useTheme } from 'next-themes'
|
|
1521
2627
|
import { useState, useEffect } from 'react'
|
|
@@ -1542,7 +2648,7 @@ export function ThemeToggle() {
|
|
|
1542
2648
|
</button>
|
|
1543
2649
|
)
|
|
1544
2650
|
}
|
|
1545
|
-
`),await
|
|
2651
|
+
`),await n(a(c,`app/nav-header.tsx`),`'use client'
|
|
1546
2652
|
|
|
1547
2653
|
import Link from 'next/link'
|
|
1548
2654
|
import { usePathname } from 'next/navigation'
|
|
@@ -1570,7 +2676,7 @@ export function NavHeader() {
|
|
|
1570
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">
|
|
1571
2677
|
<nav className="max-w-6xl mx-auto flex items-center gap-1 px-4 h-12">
|
|
1572
2678
|
<span className="font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none">
|
|
1573
|
-
${
|
|
2679
|
+
${s}
|
|
1574
2680
|
</span>
|
|
1575
2681
|
{links.map(({ href, label }) => (
|
|
1576
2682
|
<Link key={href} href={href} className={linkClass(href)}>
|
|
@@ -1584,12 +2690,12 @@ export function NavHeader() {
|
|
|
1584
2690
|
</header>
|
|
1585
2691
|
)
|
|
1586
2692
|
}
|
|
1587
|
-
`),await
|
|
2693
|
+
`),await n(a(c,`app/layout.tsx`),`import './globals.css'
|
|
1588
2694
|
|
|
1589
2695
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
1590
2696
|
return children
|
|
1591
2697
|
}
|
|
1592
|
-
`),await
|
|
2698
|
+
`),await n(a(c,`app/[locale]/layout.tsx`),`import type { Metadata } from 'next'
|
|
1593
2699
|
import { Geist, Geist_Mono } from 'next/font/google'
|
|
1594
2700
|
import { NextIntlClientProvider } from 'next-intl'
|
|
1595
2701
|
import { getMessages } from 'next-intl/server'
|
|
@@ -1609,7 +2715,7 @@ const geistMono = Geist_Mono({
|
|
|
1609
2715
|
})
|
|
1610
2716
|
|
|
1611
2717
|
export const metadata: Metadata = {
|
|
1612
|
-
title: '${
|
|
2718
|
+
title: '${s}',
|
|
1613
2719
|
description: 'Built with Lumi CMS Toolkit',
|
|
1614
2720
|
}
|
|
1615
2721
|
|
|
@@ -1638,9 +2744,9 @@ export default async function LocaleLayout({
|
|
|
1638
2744
|
</html>
|
|
1639
2745
|
)
|
|
1640
2746
|
}
|
|
1641
|
-
`),await
|
|
1642
|
-
import { Article, Category } from '@${
|
|
1643
|
-
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'
|
|
1644
2750
|
|
|
1645
2751
|
export default async function HomePage() {
|
|
1646
2752
|
await getToolkitApp()
|
|
@@ -1655,7 +2761,7 @@ export default async function HomePage() {
|
|
|
1655
2761
|
return (
|
|
1656
2762
|
<div className="min-h-screen p-8">
|
|
1657
2763
|
<div className="max-w-4xl mx-auto">
|
|
1658
|
-
<h1 className="text-4xl font-bold mb-8">Welcome to ${
|
|
2764
|
+
<h1 className="text-4xl font-bold mb-8">Welcome to ${s}</h1>
|
|
1659
2765
|
|
|
1660
2766
|
<section className="mb-12">
|
|
1661
2767
|
<h2 className="text-2xl font-semibold mb-4">
|
|
@@ -1702,7 +2808,7 @@ export default async function HomePage() {
|
|
|
1702
2808
|
</div>
|
|
1703
2809
|
)
|
|
1704
2810
|
}
|
|
1705
|
-
`)}
|
|
2811
|
+
`)}async function v(e,t){let{name:r}=t;await n(a(e,`next.config.ts`),`import type { NextConfig } from 'next'
|
|
1706
2812
|
import createNextIntlPlugin from 'next-intl/plugin'
|
|
1707
2813
|
|
|
1708
2814
|
const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
|
|
@@ -1727,7 +2833,7 @@ const nextConfig: NextConfig = {
|
|
|
1727
2833
|
}
|
|
1728
2834
|
|
|
1729
2835
|
export default withNextIntl(nextConfig)
|
|
1730
|
-
`),await
|
|
2836
|
+
`),await n(a(e,`proxy.ts`),`import { NextRequest, NextResponse } from 'next/server'
|
|
1731
2837
|
import { getSessionCookie } from 'better-auth/cookies'
|
|
1732
2838
|
import createMiddleware from 'next-intl/middleware'
|
|
1733
2839
|
import { routing } from './i18n/routing'
|
|
@@ -1759,12 +2865,12 @@ export function proxy(request: NextRequest) {
|
|
|
1759
2865
|
export const config = {
|
|
1760
2866
|
matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)',
|
|
1761
2867
|
}
|
|
1762
|
-
`),await
|
|
2868
|
+
`),await n(a(e,`app/layout.tsx`),`import './globals.css'
|
|
1763
2869
|
|
|
1764
2870
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
1765
2871
|
return children
|
|
1766
2872
|
}
|
|
1767
|
-
`),await
|
|
2873
|
+
`),await n(a(e,`app/[locale]/layout.tsx`),`import type { Metadata } from 'next'
|
|
1768
2874
|
import { Geist, Geist_Mono } from 'next/font/google'
|
|
1769
2875
|
import { NextIntlClientProvider } from 'next-intl'
|
|
1770
2876
|
import { getMessages } from 'next-intl/server'
|
|
@@ -1813,7 +2919,7 @@ export default async function LocaleLayout({
|
|
|
1813
2919
|
</html>
|
|
1814
2920
|
)
|
|
1815
2921
|
}
|
|
1816
|
-
`),await
|
|
2922
|
+
`),await n(a(e,`app/[locale]/page.tsx`),`import { createQueryClient } from '@murumets-ee/core/clients'
|
|
1817
2923
|
import { Article, Category } from '@/entities'
|
|
1818
2924
|
import { getToolkitApp } from '@/lib/app'
|
|
1819
2925
|
|
|
@@ -1877,13 +2983,49 @@ export default async function HomePage() {
|
|
|
1877
2983
|
</div>
|
|
1878
2984
|
)
|
|
1879
2985
|
}
|
|
1880
|
-
`)}
|
|
2986
|
+
`)}async function y(e,t){await n(a(e,`scripts/generate-schema.ts`),`import { execSync } from 'node:child_process'
|
|
1881
2987
|
execSync('npx @murumets-ee/cli generate', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })
|
|
1882
|
-
`),await
|
|
2988
|
+
`),await n(a(e,`scripts/migrate.ts`),`import { execSync } from 'node:child_process'
|
|
1883
2989
|
execSync('npx @murumets-ee/cli migrate', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })
|
|
1884
|
-
`),await
|
|
2990
|
+
`),await n(a(e,`scripts/reset-db.ts`),`import { execSync } from 'node:child_process'
|
|
1885
2991
|
execSync('npx @murumets-ee/cli reset --force', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })
|
|
1886
|
-
`)
|
|
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";
|
|
1887
3029
|
@source "../node_modules/@murumets-ee/auth-ui/dist";
|
|
1888
3030
|
|
|
1889
3031
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
@@ -1910,7 +3052,7 @@ body {
|
|
|
1910
3052
|
color: var(--foreground);
|
|
1911
3053
|
font-family: Arial, Helvetica, sans-serif;
|
|
1912
3054
|
}
|
|
1913
|
-
`),await
|
|
3055
|
+
`),await n(a(e,`app/theme-provider.tsx`),`'use client'
|
|
1914
3056
|
|
|
1915
3057
|
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
|
1916
3058
|
import type { ReactNode } from 'react'
|
|
@@ -1927,7 +3069,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
|
1927
3069
|
</NextThemesProvider>
|
|
1928
3070
|
)
|
|
1929
3071
|
}
|
|
1930
|
-
`),await
|
|
3072
|
+
`),await n(a(e,`app/theme-toggle.tsx`),`'use client'
|
|
1931
3073
|
|
|
1932
3074
|
import { useTheme } from 'next-themes'
|
|
1933
3075
|
import { useState, useEffect } from 'react'
|
|
@@ -1954,7 +3096,7 @@ export function ThemeToggle() {
|
|
|
1954
3096
|
</button>
|
|
1955
3097
|
)
|
|
1956
3098
|
}
|
|
1957
|
-
`),await
|
|
3099
|
+
`),await n(a(e,`app/nav-header.tsx`),`'use client'
|
|
1958
3100
|
|
|
1959
3101
|
import Link from 'next/link'
|
|
1960
3102
|
import { usePathname } from 'next/navigation'
|
|
@@ -1998,7 +3140,7 @@ export function NavHeader() {
|
|
|
1998
3140
|
</header>
|
|
1999
3141
|
)
|
|
2000
3142
|
}
|
|
2001
|
-
`)}
|
|
3143
|
+
`)}async function x(e,t){let{name:r}=t;await n(a(e,`toolkit.config.ts`),`import { auth } from '@murumets-ee/auth/plugin'
|
|
2002
3144
|
import { content } from '@murumets-ee/content/plugin'
|
|
2003
3145
|
import { defineConfig } from '@murumets-ee/core'
|
|
2004
3146
|
import { logging } from '@murumets-ee/logging/plugin'
|
|
@@ -2052,21 +3194,19 @@ export default defineConfig({
|
|
|
2052
3194
|
],
|
|
2053
3195
|
projectRoot: import.meta.dirname,
|
|
2054
3196
|
})
|
|
2055
|
-
`),await
|
|
3197
|
+
`),await n(a(e,`lib/app.ts`),`import { createApp, setApp, type ToolkitApp } from '@murumets-ee/core'
|
|
2056
3198
|
import config from '../toolkit.config'
|
|
2057
3199
|
|
|
2058
3200
|
let appInstance: ToolkitApp | null = null
|
|
2059
3201
|
|
|
2060
3202
|
export async function getToolkitApp(): Promise<ToolkitApp> {
|
|
2061
3203
|
if (!appInstance) {
|
|
2062
|
-
console.log('Initializing toolkit app...')
|
|
2063
3204
|
appInstance = await createApp(config)
|
|
2064
3205
|
setApp(appInstance)
|
|
2065
|
-
console.log('Toolkit app initialized')
|
|
2066
3206
|
}
|
|
2067
3207
|
return appInstance
|
|
2068
3208
|
}
|
|
2069
|
-
`),await
|
|
3209
|
+
`),await n(a(e,`drizzle.config.ts`),`import type { Config } from 'drizzle-kit'
|
|
2070
3210
|
|
|
2071
3211
|
if (!process.env.DATABASE_URL) {
|
|
2072
3212
|
throw new Error('DATABASE_URL environment variable is required')
|
|
@@ -2080,7 +3220,7 @@ export default {
|
|
|
2080
3220
|
url: process.env.DATABASE_URL,
|
|
2081
3221
|
},
|
|
2082
3222
|
} satisfies Config
|
|
2083
|
-
`),await
|
|
3223
|
+
`),await n(a(e,`auth.config.ts`),`import { betterAuth } from 'better-auth'
|
|
2084
3224
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
|
2085
3225
|
import { admin } from 'better-auth/plugins'
|
|
2086
3226
|
import { organization } from 'better-auth/plugins/organization'
|
|
@@ -2098,7 +3238,8 @@ export const auth = betterAuth({
|
|
|
2098
3238
|
organization(),
|
|
2099
3239
|
],
|
|
2100
3240
|
})
|
|
2101
|
-
`),await
|
|
3241
|
+
`),await n(a(e,`generated/auth-schema.ts`),`// This file is generated by better-auth CLI.
|
|
2102
3242
|
// Run: npx @better-auth/cli generate --config auth.config.ts -y
|
|
2103
3243
|
export {}
|
|
2104
|
-
`)}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
|