@murumets-ee/create 0.1.9 → 0.1.10
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.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -3
- package/dist/cli.cjs +0 -2105
- package/dist/index.cjs +0 -2103
- package/dist/index.d.cts +0 -11
package/dist/cli.cjs
DELETED
|
@@ -1,2105 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";var G=Object.create;var E=Object.defineProperty;var K=Object.getOwnPropertyDescriptor;var V=Object.getOwnPropertyNames;var J=Object.getPrototypeOf,Y=Object.prototype.hasOwnProperty;var Q=(e,o)=>()=>(e&&(o=e(e=0)),o);var X=(e,o)=>{for(var t in o)E(e,t,{get:o[t],enumerable:!0})},Z=(e,o,t,a)=>{if(o&&typeof o=="object"||typeof o=="function")for(let s of V(o))!Y.call(e,s)&&s!==t&&E(e,s,{get:()=>o[s],enumerable:!(a=K(o,s))||a.enumerable});return e};var P=(e,o,t)=>(t=e!=null?G(J(e)):{},Z(o||!e||!e.__esModule?E(t,"default",{value:e,enumerable:!0}):t,e));var z={};X(z,{appendToFile:()=>R,deleteFiles:()=>x,mergePackageJson:()=>h,runCommand:()=>p,writeFile:()=>r});async function r(e,o){await(0,u.mkdir)((0,k.dirname)(e),{recursive:!0}),await(0,u.writeFile)(e,o,"utf-8")}async function h(e,o){let t=await(0,u.readFile)(e,"utf-8"),a=JSON.parse(t);o.type&&(a.type=o.type),o.dependencies&&(a.dependencies={...a.dependencies,...o.dependencies}),o.devDependencies&&(a.devDependencies={...a.devDependencies,...o.devDependencies}),o.scripts&&(a.scripts={...a.scripts,...o.scripts}),await(0,u.writeFile)(e,`${JSON.stringify(a,null,2)}
|
|
3
|
-
`,"utf-8")}async function x(e,o){for(let t of o)await(0,u.rm)((0,k.join)(e,t),{force:!0,recursive:!0})}function p(e,o){return(0,M.execSync)(e,{cwd:o?.cwd,stdio:"pipe",encoding:"utf-8"})}async function R(e,o){await(0,u.appendFile)(e,o,"utf-8")}var M,u,k,c=Q(()=>{"use strict";M=require("child_process"),u=require("fs/promises"),k=require("path")});var d=P(require("@clack/prompts"),1),w=P(require("picocolors"),1);var l=P(require("@clack/prompts"),1);function ee(e){let o={},t=e.slice(2);for(let a=0;a<t.length;a++){let s=t[a];if(s==="--mode"&&t[a+1]){let f=t[++a];(f==="single"||f==="multi")&&(o.mode=f)}else s==="--install"?o.install=!0:s==="--no-install"?o.install=!1:!s.startsWith("-")&&!o.name&&(o.name=s)}return o}async function $(e){let o=ee(process.argv),t=o.name??e;if(t&&o.mode&&o.install!==void 0)return{name:t,mode:o.mode,installDeps:o.install};l.intro("@murumets-ee/create");let a=t??await l.text({message:"Project name:",placeholder:"my-project",validate:_=>{if(!_)return"Project name is required";if(!/^[a-z0-9-]+$/.test(_))return"Use lowercase letters, numbers, and hyphens only"}});if(l.isCancel(a))return l.cancel("Cancelled."),null;let s=o.mode??await l.select({message:"App setup:",options:[{value:"single",label:"Single app",hint:"everything in one Next.js project"},{value:"multi",label:"Admin + Frontend",hint:"two apps in a pnpm workspace"}]});if(l.isCancel(s))return l.cancel("Cancelled."),null;let f=o.install??await l.confirm({message:"Install dependencies?",initialValue:!0});return l.isCancel(f)?(l.cancel("Cancelled."),null):{name:a,mode:s,installDeps:f}}var S=require("path");var m=require("path");c();async function O(e,o){await r((0,m.join)(e,"lib/auth.ts"),`import { getToolkitApp } from './app'
|
|
4
|
-
import { getAuth } from '@murumets-ee/auth'
|
|
5
|
-
|
|
6
|
-
export async function getAuthInstance() {
|
|
7
|
-
await getToolkitApp()
|
|
8
|
-
return getAuth()
|
|
9
|
-
}
|
|
10
|
-
`),await r((0,m.join)(e,"lib/auth-client.ts"),`import { createClient } from '@murumets-ee/auth/client'
|
|
11
|
-
|
|
12
|
-
export const authClient = createClient()
|
|
13
|
-
`),await r((0,m.join)(e,"app/api/auth/[...all]/route.ts"),`import { toNextJsHandler } from 'better-auth/next-js'
|
|
14
|
-
import { getAuthInstance } from '../../../../lib/auth'
|
|
15
|
-
|
|
16
|
-
let _handler: ReturnType<typeof toNextJsHandler> | null = null
|
|
17
|
-
|
|
18
|
-
async function handler() {
|
|
19
|
-
if (!_handler) {
|
|
20
|
-
const auth = await getAuthInstance()
|
|
21
|
-
_handler = toNextJsHandler(auth)
|
|
22
|
-
}
|
|
23
|
-
return _handler
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function GET(req: Request) {
|
|
27
|
-
const h = await handler()
|
|
28
|
-
return h.GET(req)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function POST(req: Request) {
|
|
32
|
-
const h = await handler()
|
|
33
|
-
return h.POST(req)
|
|
34
|
-
}
|
|
35
|
-
`),await r((0,m.join)(e,"app/[locale]/auth/layout.tsx"),`import { AuthProviders } from './providers'
|
|
36
|
-
import type { ReactNode } from 'react'
|
|
37
|
-
|
|
38
|
-
export default function AuthLayout({ children }: { children: ReactNode }) {
|
|
39
|
-
return (
|
|
40
|
-
<AuthProviders>
|
|
41
|
-
<div className="flex min-h-[calc(100vh-3rem)] items-center justify-center px-4">
|
|
42
|
-
{children}
|
|
43
|
-
</div>
|
|
44
|
-
</AuthProviders>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
`),await r((0,m.join)(e,"app/[locale]/auth/providers.tsx"),`'use client'
|
|
48
|
-
|
|
49
|
-
import { AuthUIProvider } from '@murumets-ee/auth-ui'
|
|
50
|
-
import { authClient } from '@/lib/auth-client'
|
|
51
|
-
import Link from 'next/link'
|
|
52
|
-
import { useRouter } from 'next/navigation'
|
|
53
|
-
import { useLocale } from 'next-intl'
|
|
54
|
-
import type { ReactNode } from 'react'
|
|
55
|
-
|
|
56
|
-
export function AuthProviders({ children }: { children: ReactNode }) {
|
|
57
|
-
const router = useRouter()
|
|
58
|
-
const locale = useLocale()
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<AuthUIProvider
|
|
62
|
-
authClient={authClient as never}
|
|
63
|
-
basePath="/auth"
|
|
64
|
-
redirectTo="/"
|
|
65
|
-
Link={Link}
|
|
66
|
-
navigate={(url) => router.push(url)}
|
|
67
|
-
resetPasswordUrl={\`/\${locale}/auth/reset-password\`}
|
|
68
|
-
>
|
|
69
|
-
{children}
|
|
70
|
-
</AuthUIProvider>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
`),await r((0,m.join)(e,"app/[locale]/auth/sign-in/page.tsx"),`import { SignInForm } from '@murumets-ee/auth-ui'
|
|
74
|
-
|
|
75
|
-
export default function SignInPage() {
|
|
76
|
-
return <SignInForm />
|
|
77
|
-
}
|
|
78
|
-
`),await r((0,m.join)(e,"app/[locale]/auth/sign-up/page.tsx"),`import { SignUpForm } from '@murumets-ee/auth-ui'
|
|
79
|
-
|
|
80
|
-
export default function SignUpPage() {
|
|
81
|
-
return <SignUpForm />
|
|
82
|
-
}
|
|
83
|
-
`),await r((0,m.join)(e,"app/[locale]/auth/forgot-password/page.tsx"),`import { ForgotPasswordForm } from '@murumets-ee/auth-ui'
|
|
84
|
-
|
|
85
|
-
export default function ForgotPasswordPage() {
|
|
86
|
-
return <ForgotPasswordForm />
|
|
87
|
-
}
|
|
88
|
-
`),await r((0,m.join)(e,"app/[locale]/auth/reset-password/page.tsx"),`import { Suspense } from 'react'
|
|
89
|
-
import { ResetPasswordContent } from './content'
|
|
90
|
-
|
|
91
|
-
export default function ResetPasswordPage() {
|
|
92
|
-
return (
|
|
93
|
-
<Suspense>
|
|
94
|
-
<ResetPasswordContent />
|
|
95
|
-
</Suspense>
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
`),await r((0,m.join)(e,"app/[locale]/auth/reset-password/content.tsx"),`'use client'
|
|
99
|
-
|
|
100
|
-
import { useSearchParams } from 'next/navigation'
|
|
101
|
-
import { ResetPasswordForm } from '@murumets-ee/auth-ui'
|
|
102
|
-
|
|
103
|
-
export function ResetPasswordContent() {
|
|
104
|
-
const searchParams = useSearchParams()
|
|
105
|
-
const token = searchParams.get('token')
|
|
106
|
-
|
|
107
|
-
if (!token) {
|
|
108
|
-
return (
|
|
109
|
-
<div className="text-center">
|
|
110
|
-
<h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50">
|
|
111
|
-
Invalid or expired link
|
|
112
|
-
</h1>
|
|
113
|
-
<p className="mt-2 text-zinc-500">
|
|
114
|
-
Please request a new password reset.
|
|
115
|
-
</p>
|
|
116
|
-
</div>
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return <ResetPasswordForm token={token} />
|
|
121
|
-
}
|
|
122
|
-
`),await r((0,m.join)(e,"app/[locale]/setup/page.tsx"),`import { redirect } from 'next/navigation'
|
|
123
|
-
import { sql } from 'drizzle-orm'
|
|
124
|
-
import { getToolkitApp } from '@/lib/app'
|
|
125
|
-
import { SetupForm } from './form'
|
|
126
|
-
|
|
127
|
-
export default async function SetupPage() {
|
|
128
|
-
const app = await getToolkitApp()
|
|
129
|
-
const result = await app.db.readOnly.execute<{ count: string }>(
|
|
130
|
-
sql\`SELECT COUNT(*)::text as count FROM "user"\`,
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
if (Number(result[0]?.count) > 0) {
|
|
134
|
-
redirect('/')
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return (
|
|
138
|
-
<div className="max-w-md mx-auto p-8">
|
|
139
|
-
<h1 className="text-2xl font-bold mb-2">Create Admin</h1>
|
|
140
|
-
<p className="text-sm text-zinc-500 dark:text-zinc-400 mb-6">
|
|
141
|
-
No users found. Create the first admin account.
|
|
142
|
-
</p>
|
|
143
|
-
<SetupForm />
|
|
144
|
-
</div>
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
`),await r((0,m.join)(e,"app/[locale]/setup/form.tsx"),`'use client'
|
|
148
|
-
|
|
149
|
-
import { useState } from 'react'
|
|
150
|
-
import { createFirstAdmin } from './actions'
|
|
151
|
-
|
|
152
|
-
export function SetupForm() {
|
|
153
|
-
const [result, setResult] = useState<{ ok?: boolean; error?: string; email?: string } | null>(null)
|
|
154
|
-
const [loading, setLoading] = useState(false)
|
|
155
|
-
|
|
156
|
-
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
157
|
-
e.preventDefault()
|
|
158
|
-
setLoading(true)
|
|
159
|
-
setResult(null)
|
|
160
|
-
const res = await createFirstAdmin(new FormData(e.currentTarget))
|
|
161
|
-
setResult(res)
|
|
162
|
-
setLoading(false)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (result?.ok) {
|
|
166
|
-
return (
|
|
167
|
-
<div className="space-y-2">
|
|
168
|
-
<p className="text-green-600 dark:text-green-400">Admin created: {result.email}</p>
|
|
169
|
-
<a href="/auth/sign-in" className="text-blue-600 dark:text-blue-400 hover:underline">
|
|
170
|
-
Sign in →
|
|
171
|
-
</a>
|
|
172
|
-
</div>
|
|
173
|
-
)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return (
|
|
177
|
-
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
178
|
-
<label className="text-sm font-medium">
|
|
179
|
-
Name
|
|
180
|
-
<input
|
|
181
|
-
name="name"
|
|
182
|
-
required
|
|
183
|
-
className="mt-1 block w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
184
|
-
/>
|
|
185
|
-
</label>
|
|
186
|
-
<label className="text-sm font-medium">
|
|
187
|
-
Email
|
|
188
|
-
<input
|
|
189
|
-
name="email"
|
|
190
|
-
type="email"
|
|
191
|
-
required
|
|
192
|
-
className="mt-1 block w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
193
|
-
/>
|
|
194
|
-
</label>
|
|
195
|
-
<label className="text-sm font-medium">
|
|
196
|
-
Password (min 8 chars)
|
|
197
|
-
<input
|
|
198
|
-
name="password"
|
|
199
|
-
type="password"
|
|
200
|
-
required
|
|
201
|
-
minLength={8}
|
|
202
|
-
className="mt-1 block w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
203
|
-
/>
|
|
204
|
-
</label>
|
|
205
|
-
|
|
206
|
-
{result?.error && <p className="text-red-600 dark:text-red-400 text-sm">{result.error}</p>}
|
|
207
|
-
|
|
208
|
-
<button
|
|
209
|
-
type="submit"
|
|
210
|
-
disabled={loading}
|
|
211
|
-
className="px-4 py-2 text-sm rounded-md bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-50 transition-colors"
|
|
212
|
-
>
|
|
213
|
-
{loading ? 'Creating...' : 'Create Admin'}
|
|
214
|
-
</button>
|
|
215
|
-
</form>
|
|
216
|
-
)
|
|
217
|
-
}
|
|
218
|
-
`),await r((0,m.join)(e,"app/[locale]/setup/actions.ts"),`'use server'
|
|
219
|
-
|
|
220
|
-
import { sql } from 'drizzle-orm'
|
|
221
|
-
import { getToolkitApp } from '@/lib/app'
|
|
222
|
-
import { getAuth } from '@murumets-ee/auth'
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Create the first admin user. Locked down:
|
|
226
|
-
* - Advisory lock prevents race conditions (two simultaneous requests)
|
|
227
|
-
* - User count check inside the lock ensures only one admin can be created
|
|
228
|
-
* - Once any user exists, this action permanently refuses
|
|
229
|
-
*/
|
|
230
|
-
export async function createFirstAdmin(formData: FormData) {
|
|
231
|
-
const email = formData.get('email') as string
|
|
232
|
-
const password = formData.get('password') as string
|
|
233
|
-
const name = formData.get('name') as string
|
|
234
|
-
|
|
235
|
-
if (!email || !password || !name) {
|
|
236
|
-
return { error: 'All fields are required' }
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (password.length < 8) {
|
|
240
|
-
return { error: 'Password must be at least 8 characters' }
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const app = await getToolkitApp()
|
|
244
|
-
|
|
245
|
-
// Acquire advisory lock + check atomically in a transaction
|
|
246
|
-
const canCreate = await app.db.readWrite.transaction(async (tx) => {
|
|
247
|
-
// Advisory lock 1 = "setup lock". Blocks concurrent setup attempts.
|
|
248
|
-
await tx.execute(sql\`SELECT pg_advisory_xact_lock(1)\`)
|
|
249
|
-
|
|
250
|
-
const result = await tx.execute<{ count: string }>(
|
|
251
|
-
sql\`SELECT COUNT(*)::text as count FROM "user"\`,
|
|
252
|
-
)
|
|
253
|
-
return Number(result[0]?.count) === 0
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
if (!canCreate) {
|
|
257
|
-
return { error: 'Admin user already exists. Setup is complete.' }
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const auth = getAuth()
|
|
261
|
-
|
|
262
|
-
const adminUser = await auth.api.signUpEmail({
|
|
263
|
-
body: { email, password, name },
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
await app.db.readWrite.execute(
|
|
267
|
-
sql\`UPDATE "user" SET role = 'admin' WHERE id = \${adminUser.user.id}\`,
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
return { ok: true, email: adminUser.user.email }
|
|
271
|
-
}
|
|
272
|
-
`)}var g=require("path");c();async function q(e,o){let{name:t}=o;await r((0,g.join)(e,".env.example"),`DATABASE_URL=postgresql://${t}:${t}_dev_password@localhost:5432/${t}_dev
|
|
273
|
-
BETTER_AUTH_SECRET=dev-secret-change-me-in-production-min-32-chars
|
|
274
|
-
BETTER_AUTH_URL=http://localhost:3000
|
|
275
|
-
LOG_LEVEL=debug
|
|
276
|
-
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
277
|
-
QUEUE_WORKER=true
|
|
278
|
-
RESEND_API_KEY=
|
|
279
|
-
RESEND_WEBHOOK_SECRET=
|
|
280
|
-
MAIL_FROM=noreply@example.com
|
|
281
|
-
CSAT_SECRET=
|
|
282
|
-
`),await r((0,g.join)(e,".dockerignore"),`# Dependencies (installed inside Docker)
|
|
283
|
-
node_modules/
|
|
284
|
-
.pnpm-store/
|
|
285
|
-
|
|
286
|
-
# Build outputs (rebuilt inside Docker)
|
|
287
|
-
.next/
|
|
288
|
-
out/
|
|
289
|
-
dist/
|
|
290
|
-
|
|
291
|
-
# Git
|
|
292
|
-
.git/
|
|
293
|
-
.gitignore
|
|
294
|
-
|
|
295
|
-
# Environment files (pass via docker run --env-file)
|
|
296
|
-
.env
|
|
297
|
-
.env.*
|
|
298
|
-
!.env.example
|
|
299
|
-
|
|
300
|
-
# Docker files
|
|
301
|
-
Dockerfile*
|
|
302
|
-
.dockerignore
|
|
303
|
-
docker-compose*.yml
|
|
304
|
-
|
|
305
|
-
# Tests
|
|
306
|
-
coverage/
|
|
307
|
-
**/*.test.*
|
|
308
|
-
**/*.spec.*
|
|
309
|
-
**/vitest.config.*
|
|
310
|
-
docker-compose.test.yml
|
|
311
|
-
|
|
312
|
-
# Development tooling
|
|
313
|
-
.vscode/
|
|
314
|
-
.idea/
|
|
315
|
-
.turbo/
|
|
316
|
-
.cache/
|
|
317
|
-
*.tsbuildinfo
|
|
318
|
-
|
|
319
|
-
# Documentation
|
|
320
|
-
*.md
|
|
321
|
-
docs/
|
|
322
|
-
|
|
323
|
-
# OS files
|
|
324
|
-
.DS_Store
|
|
325
|
-
Thumbs.db
|
|
326
|
-
`),await r((0,g.join)(e,"Dockerfile"),`# --- Base image ---
|
|
327
|
-
# Node 22 LTS (Debian slim \u2014 glibc required for sharp image optimization)
|
|
328
|
-
FROM node:22-slim AS base
|
|
329
|
-
|
|
330
|
-
# --- Stage 1: Install dependencies ---
|
|
331
|
-
FROM base AS deps
|
|
332
|
-
WORKDIR /app
|
|
333
|
-
RUN corepack enable pnpm
|
|
334
|
-
COPY package.json pnpm-lock.yaml ./
|
|
335
|
-
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \\
|
|
336
|
-
pnpm install --frozen-lockfile
|
|
337
|
-
|
|
338
|
-
# --- Stage 2: Build the application ---
|
|
339
|
-
FROM base AS builder
|
|
340
|
-
WORKDIR /app
|
|
341
|
-
RUN corepack enable pnpm
|
|
342
|
-
COPY --from=deps /app/node_modules ./node_modules
|
|
343
|
-
COPY . .
|
|
344
|
-
ENV NODE_ENV=production
|
|
345
|
-
ENV NEXT_TELEMETRY_DISABLED=1
|
|
346
|
-
# Dummy env vars so toolkit.config.ts doesn't throw at build time (no DB connection needed)
|
|
347
|
-
ENV DATABASE_URL=postgresql://build:build@localhost:5432/build
|
|
348
|
-
ENV BETTER_AUTH_SECRET=build-secret-not-used-at-runtime
|
|
349
|
-
RUN pnpm build
|
|
350
|
-
|
|
351
|
-
# --- Stage 3: Production runner ---
|
|
352
|
-
FROM base AS runner
|
|
353
|
-
WORKDIR /app
|
|
354
|
-
ENV NODE_ENV=production
|
|
355
|
-
ENV PORT=3000
|
|
356
|
-
ENV HOSTNAME=0.0.0.0
|
|
357
|
-
ENV NEXT_TELEMETRY_DISABLED=1
|
|
358
|
-
|
|
359
|
-
# Copy standalone output (includes server.js + traced node_modules)
|
|
360
|
-
COPY --from=builder --chown=node:node /app/.next/standalone ./
|
|
361
|
-
# Copy static assets (JS/CSS chunks \u2014 excluded from standalone trace)
|
|
362
|
-
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
|
|
363
|
-
# Copy public assets (favicon, robots.txt, images)
|
|
364
|
-
COPY --from=builder --chown=node:node /app/public ./public
|
|
365
|
-
# Copy migrations (applied at app startup by the toolkit migration runner)
|
|
366
|
-
COPY --from=builder --chown=node:node /app/migrations ./migrations
|
|
367
|
-
|
|
368
|
-
# Run as non-root user
|
|
369
|
-
USER node
|
|
370
|
-
EXPOSE 3000
|
|
371
|
-
CMD ["node", "server.js"]
|
|
372
|
-
`),await r((0,g.join)(e,"docker-compose.yml"),`services:
|
|
373
|
-
postgres:
|
|
374
|
-
image: postgres:17-alpine
|
|
375
|
-
container_name: ${t}-postgres
|
|
376
|
-
restart: unless-stopped
|
|
377
|
-
environment:
|
|
378
|
-
POSTGRES_USER: ${t}
|
|
379
|
-
POSTGRES_PASSWORD: ${t}_dev_password
|
|
380
|
-
POSTGRES_DB: ${t}_dev
|
|
381
|
-
ports:
|
|
382
|
-
- "5432:5432"
|
|
383
|
-
volumes:
|
|
384
|
-
- postgres_data:/var/lib/postgresql/data
|
|
385
|
-
healthcheck:
|
|
386
|
-
test: ["CMD-SHELL", "pg_isready -U ${t} -d ${t}_dev"]
|
|
387
|
-
interval: 10s
|
|
388
|
-
timeout: 5s
|
|
389
|
-
retries: 5
|
|
390
|
-
|
|
391
|
-
volumes:
|
|
392
|
-
postgres_data:
|
|
393
|
-
name: ${t}_postgres_data
|
|
394
|
-
`),await r((0,g.join)(e,"docker-compose.prod.yml"),`services:
|
|
395
|
-
app:
|
|
396
|
-
build: .
|
|
397
|
-
restart: unless-stopped
|
|
398
|
-
env_file: .env
|
|
399
|
-
depends_on:
|
|
400
|
-
postgres:
|
|
401
|
-
condition: service_healthy
|
|
402
|
-
ports:
|
|
403
|
-
- "3000:3000"
|
|
404
|
-
networks:
|
|
405
|
-
- internal
|
|
406
|
-
|
|
407
|
-
postgres:
|
|
408
|
-
image: postgres:17-alpine
|
|
409
|
-
restart: unless-stopped
|
|
410
|
-
environment:
|
|
411
|
-
POSTGRES_USER: ${t}
|
|
412
|
-
POSTGRES_PASSWORD: \${DB_PASSWORD}
|
|
413
|
-
POSTGRES_DB: ${t}
|
|
414
|
-
volumes:
|
|
415
|
-
- postgres_data:/var/lib/postgresql/data
|
|
416
|
-
healthcheck:
|
|
417
|
-
test: ["CMD-SHELL", "pg_isready -U ${t} -d ${t}"]
|
|
418
|
-
interval: 10s
|
|
419
|
-
timeout: 5s
|
|
420
|
-
retries: 5
|
|
421
|
-
networks:
|
|
422
|
-
- internal
|
|
423
|
-
|
|
424
|
-
volumes:
|
|
425
|
-
postgres_data:
|
|
426
|
-
|
|
427
|
-
networks:
|
|
428
|
-
internal:
|
|
429
|
-
`),await R((0,g.join)(e,".gitignore"),`
|
|
430
|
-
# Toolkit generated files
|
|
431
|
-
generated/
|
|
432
|
-
|
|
433
|
-
# Environment
|
|
434
|
-
.env*
|
|
435
|
-
!.env.example
|
|
436
|
-
`)}var A=require("path");c();async function I(e,o){await r((0,A.join)(e,"entities/index.ts"),`export { Article } from './article'
|
|
437
|
-
export { Category } from './category'
|
|
438
|
-
`),await r((0,A.join)(e,"entities/article.ts"),`import { behavior, defineEntity, field } from '@murumets-ee/entity'
|
|
439
|
-
|
|
440
|
-
export const Article = defineEntity({
|
|
441
|
-
name: 'article',
|
|
442
|
-
fields: {
|
|
443
|
-
title: field.text({ required: true, maxLength: 200, indexed: true, translatable: true }),
|
|
444
|
-
slug: field.slug({ from: 'title', unique: true }),
|
|
445
|
-
excerpt: field.text({ maxLength: 500, translatable: true }),
|
|
446
|
-
body: field.richtext(),
|
|
447
|
-
viewCount: field.number({ default: 0, integer: true }),
|
|
448
|
-
featured: field.boolean({ default: false }),
|
|
449
|
-
publishDate: field.date(),
|
|
450
|
-
contentType: field.select({ options: ['news', 'tutorial', 'announcement'], default: 'news' }),
|
|
451
|
-
category: field.reference({ entity: 'category', required: false }),
|
|
452
|
-
tags: field.reference({ entity: 'category', cardinality: 'many' }),
|
|
453
|
-
coverImage: field.media({ accept: ['image/*'] }),
|
|
454
|
-
},
|
|
455
|
-
behaviors: [
|
|
456
|
-
behavior.publishable(),
|
|
457
|
-
behavior.auditable(),
|
|
458
|
-
behavior.sluggable('title'),
|
|
459
|
-
behavior.revisionable(),
|
|
460
|
-
],
|
|
461
|
-
scope: 'global',
|
|
462
|
-
access: {
|
|
463
|
-
view: 'public',
|
|
464
|
-
create: 'group.editor',
|
|
465
|
-
update: 'group.editor',
|
|
466
|
-
delete: 'group.admin',
|
|
467
|
-
},
|
|
468
|
-
})
|
|
469
|
-
`),await r((0,A.join)(e,"entities/category.ts"),`import { defineEntity, field } from '@murumets-ee/entity'
|
|
470
|
-
|
|
471
|
-
export const Category = defineEntity({
|
|
472
|
-
name: 'category',
|
|
473
|
-
fields: {
|
|
474
|
-
name: field.text({ required: true, maxLength: 100, indexed: true, translatable: true }),
|
|
475
|
-
slug: field.slug({ from: 'name', unique: true }),
|
|
476
|
-
description: field.text({ maxLength: 500, translatable: true }),
|
|
477
|
-
},
|
|
478
|
-
scope: 'global',
|
|
479
|
-
access: {
|
|
480
|
-
view: 'public',
|
|
481
|
-
create: 'group.editor',
|
|
482
|
-
update: 'group.editor',
|
|
483
|
-
delete: 'group.admin',
|
|
484
|
-
},
|
|
485
|
-
})
|
|
486
|
-
`)}var C=require("path");c();async function F(e,o){await r((0,C.join)(e,"i18n/routing.ts"),`import { defineRouting } from 'next-intl/routing'
|
|
487
|
-
|
|
488
|
-
export const routing = defineRouting({
|
|
489
|
-
locales: ['en'],
|
|
490
|
-
defaultLocale: 'en',
|
|
491
|
-
})
|
|
492
|
-
`),await r((0,C.join)(e,"i18n/request.ts"),`import { getRequestConfig } from 'next-intl/server'
|
|
493
|
-
import { hasLocale } from 'next-intl'
|
|
494
|
-
import { routing } from './routing'
|
|
495
|
-
import { getAuthMessages } from '@murumets-ee/auth-ui/i18n'
|
|
496
|
-
|
|
497
|
-
export default getRequestConfig(async ({ requestLocale }) => {
|
|
498
|
-
const requested = await requestLocale
|
|
499
|
-
const locale = hasLocale(routing.locales, requested)
|
|
500
|
-
? requested
|
|
501
|
-
: routing.defaultLocale
|
|
502
|
-
|
|
503
|
-
const authMessages = await getAuthMessages(locale)
|
|
504
|
-
|
|
505
|
-
return {
|
|
506
|
-
locale,
|
|
507
|
-
messages: {
|
|
508
|
-
...authMessages,
|
|
509
|
-
},
|
|
510
|
-
}
|
|
511
|
-
})
|
|
512
|
-
`)}var L=require("fs/promises"),i=require("path");c();var n={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",drizzleKit:"0.27.2",tsx:"4.19.2",babelReactCompiler:"1.0.0"};async function U(e,o,t){await te(e,o),t?.("Workspace root created"),t?.("Creating admin app..."),await oe(e,o),t?.("Admin app created"),t?.("Creating web app..."),await ae(e,o),t?.("Web app created"),await re(e,o),t?.("Shared config package created")}async function te(e,o){let{name:t}=o;await(0,L.mkdir)(e,{recursive:!0}),await r((0,i.join)(e,"pnpm-workspace.yaml"),`packages:
|
|
513
|
-
- 'packages/*'
|
|
514
|
-
- 'apps/*'
|
|
515
|
-
|
|
516
|
-
ignoredBuiltDependencies:
|
|
517
|
-
- sharp
|
|
518
|
-
- unrs-resolver
|
|
519
|
-
`),await r((0,i.join)(e,"package.json"),`${JSON.stringify({name:`@${t}/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:`^${n.tsx}`,"drizzle-kit":`^${n.drizzleKit}`}},null,2)}
|
|
520
|
-
`),await r((0,i.join)(e,".gitignore"),`# dependencies
|
|
521
|
-
node_modules/
|
|
522
|
-
|
|
523
|
-
# build
|
|
524
|
-
dist/
|
|
525
|
-
.next/
|
|
526
|
-
.turbo/
|
|
527
|
-
|
|
528
|
-
# env
|
|
529
|
-
.env*
|
|
530
|
-
!.env.example
|
|
531
|
-
|
|
532
|
-
# toolkit generated
|
|
533
|
-
generated/
|
|
534
|
-
|
|
535
|
-
# IDE
|
|
536
|
-
.vscode/
|
|
537
|
-
.idea/
|
|
538
|
-
|
|
539
|
-
# OS
|
|
540
|
-
.DS_Store
|
|
541
|
-
Thumbs.db
|
|
542
|
-
|
|
543
|
-
# logs
|
|
544
|
-
*.log
|
|
545
|
-
|
|
546
|
-
# migrations meta
|
|
547
|
-
migrations/
|
|
548
|
-
|
|
549
|
-
# turbo
|
|
550
|
-
.turbo/
|
|
551
|
-
`),await r((0,i.join)(e,".env.example"),`DATABASE_URL=postgresql://${t}:${t}_dev_password@localhost:5432/${t}_dev
|
|
552
|
-
BETTER_AUTH_SECRET=dev-secret-change-me-in-production-min-32-chars
|
|
553
|
-
BETTER_AUTH_URL=http://localhost:3000
|
|
554
|
-
LOG_LEVEL=debug
|
|
555
|
-
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
556
|
-
QUEUE_WORKER=true
|
|
557
|
-
RESEND_API_KEY=
|
|
558
|
-
RESEND_WEBHOOK_SECRET=
|
|
559
|
-
MAIL_FROM=noreply@example.com
|
|
560
|
-
CSAT_SECRET=
|
|
561
|
-
`),await r((0,i.join)(e,"docker-compose.yml"),`services:
|
|
562
|
-
postgres:
|
|
563
|
-
image: postgres:17-alpine
|
|
564
|
-
container_name: ${t}-postgres
|
|
565
|
-
restart: unless-stopped
|
|
566
|
-
environment:
|
|
567
|
-
POSTGRES_USER: ${t}
|
|
568
|
-
POSTGRES_PASSWORD: ${t}_dev_password
|
|
569
|
-
POSTGRES_DB: ${t}_dev
|
|
570
|
-
ports:
|
|
571
|
-
- "5432:5432"
|
|
572
|
-
volumes:
|
|
573
|
-
- postgres_data:/var/lib/postgresql/data
|
|
574
|
-
healthcheck:
|
|
575
|
-
test: ["CMD-SHELL", "pg_isready -U ${t} -d ${t}_dev"]
|
|
576
|
-
interval: 10s
|
|
577
|
-
timeout: 5s
|
|
578
|
-
retries: 5
|
|
579
|
-
|
|
580
|
-
volumes:
|
|
581
|
-
postgres_data:
|
|
582
|
-
name: ${t}_postgres_data
|
|
583
|
-
`)}async function re(e,o){let{name:t}=o,a=(0,i.join)(e,"packages/config");await r((0,i.join)(a,"package.json"),`${JSON.stringify({name:`@${t}/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":`^${n.myorgCore}`,"@murumets-ee/db":`^${n.myorgDb}`,"@murumets-ee/entity":`^${n.myorgEntity}`,"@murumets-ee/logging":`^${n.myorgLogging}`,"@murumets-ee/auth":`^${n.myorgAuth}`,"better-auth":`^${n.betterAuth}`,"drizzle-orm":`^${n.drizzleOrm}`,postgres:`^${n.postgres}`,zod:`^${n.zod}`}},null,2)}
|
|
584
|
-
`),await r((0,i.join)(a,"tsconfig.json"),`${JSON.stringify({compilerOptions:{target:"ES2022",module:"ESNext",moduleResolution:"Bundler",strict:!0,esModuleInterop:!0,skipLibCheck:!0,resolveJsonModule:!0},include:["**/*.ts"]},null,2)}
|
|
585
|
-
`),await r((0,i.join)(a,"toolkit.config.ts"),`import { auth } from '@murumets-ee/auth/plugin'
|
|
586
|
-
import { content } from '@murumets-ee/content/plugin'
|
|
587
|
-
import { defineConfig } from '@murumets-ee/core'
|
|
588
|
-
import { logging } from '@murumets-ee/logging/plugin'
|
|
589
|
-
import { mail, ResendMailProvider } from '@murumets-ee/mail'
|
|
590
|
-
import { media } from '@murumets-ee/media/plugin'
|
|
591
|
-
import { queue } from '@murumets-ee/queue/plugin'
|
|
592
|
-
import { settings } from '@murumets-ee/settings/plugin'
|
|
593
|
-
import { storage } from '@murumets-ee/storage/plugin'
|
|
594
|
-
import { taxonomy } from '@murumets-ee/taxonomy/plugin'
|
|
595
|
-
import { ticketing } from '@murumets-ee/ticketing/plugin'
|
|
596
|
-
import { Article, Category } from './entities'
|
|
597
|
-
import * as authSchema from './generated/auth-schema'
|
|
598
|
-
|
|
599
|
-
if (!process.env.DATABASE_URL) {
|
|
600
|
-
throw new Error('DATABASE_URL environment variable is required')
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
export default defineConfig({
|
|
604
|
-
db: {
|
|
605
|
-
url: process.env.DATABASE_URL,
|
|
606
|
-
poolMin: 2,
|
|
607
|
-
poolMax: 10,
|
|
608
|
-
},
|
|
609
|
-
logging: {
|
|
610
|
-
level: (process.env.LOG_LEVEL || 'info') as 'debug' | 'info' | 'warn' | 'error',
|
|
611
|
-
name: '${t}',
|
|
612
|
-
},
|
|
613
|
-
entities: [Category, Article],
|
|
614
|
-
plugins: [
|
|
615
|
-
auth({ providers: ['email'], schema: authSchema }),
|
|
616
|
-
content({
|
|
617
|
-
locales: [{ code: 'en', label: 'English' }],
|
|
618
|
-
defaultLocale: 'en',
|
|
619
|
-
}),
|
|
620
|
-
logging(),
|
|
621
|
-
settings(),
|
|
622
|
-
storage(),
|
|
623
|
-
media(),
|
|
624
|
-
taxonomy(),
|
|
625
|
-
queue(),
|
|
626
|
-
mail({
|
|
627
|
-
provider: process.env.RESEND_API_KEY
|
|
628
|
-
? new ResendMailProvider({ apiKey: process.env.RESEND_API_KEY })
|
|
629
|
-
: undefined,
|
|
630
|
-
defaultFrom: process.env.MAIL_FROM ?? 'noreply@example.com',
|
|
631
|
-
webhookSecret: process.env.RESEND_WEBHOOK_SECRET,
|
|
632
|
-
}),
|
|
633
|
-
ticketing({
|
|
634
|
-
csatSecret: process.env.CSAT_SECRET,
|
|
635
|
-
}),
|
|
636
|
-
],
|
|
637
|
-
projectRoot: import.meta.dirname,
|
|
638
|
-
})
|
|
639
|
-
`),await r((0,i.join)(a,"auth.config.ts"),`import { betterAuth } from 'better-auth'
|
|
640
|
-
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
|
641
|
-
import { admin } from 'better-auth/plugins'
|
|
642
|
-
import { organization } from 'better-auth/plugins/organization'
|
|
643
|
-
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
644
|
-
import postgres from 'postgres'
|
|
645
|
-
|
|
646
|
-
const sql = postgres(process.env.DATABASE_URL!)
|
|
647
|
-
const db = drizzle(sql)
|
|
648
|
-
|
|
649
|
-
export const auth = betterAuth({
|
|
650
|
-
database: drizzleAdapter(db, { provider: 'pg' }),
|
|
651
|
-
emailAndPassword: { enabled: true },
|
|
652
|
-
plugins: [
|
|
653
|
-
admin(),
|
|
654
|
-
organization(),
|
|
655
|
-
],
|
|
656
|
-
})
|
|
657
|
-
`),await r((0,i.join)(a,"drizzle.config.ts"),`import type { Config } from 'drizzle-kit'
|
|
658
|
-
|
|
659
|
-
if (!process.env.DATABASE_URL) {
|
|
660
|
-
throw new Error('DATABASE_URL environment variable is required')
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
export default {
|
|
664
|
-
schema: ['./generated/schema.ts', './generated/auth-schema.ts'],
|
|
665
|
-
out: './migrations',
|
|
666
|
-
dialect: 'postgresql',
|
|
667
|
-
dbCredentials: {
|
|
668
|
-
url: process.env.DATABASE_URL,
|
|
669
|
-
},
|
|
670
|
-
} satisfies Config
|
|
671
|
-
`),await r((0,i.join)(a,"entities/index.ts"),`export { Article } from './article'
|
|
672
|
-
export { Category } from './category'
|
|
673
|
-
`),await r((0,i.join)(a,"entities/article.ts"),`import { behavior, defineEntity, field } from '@murumets-ee/entity'
|
|
674
|
-
|
|
675
|
-
export const Article = defineEntity({
|
|
676
|
-
name: 'article',
|
|
677
|
-
fields: {
|
|
678
|
-
title: field.text({ required: true, maxLength: 200, indexed: true, translatable: true }),
|
|
679
|
-
slug: field.slug({ from: 'title', unique: true }),
|
|
680
|
-
excerpt: field.text({ maxLength: 500, translatable: true }),
|
|
681
|
-
body: field.richtext(),
|
|
682
|
-
viewCount: field.number({ default: 0, integer: true }),
|
|
683
|
-
featured: field.boolean({ default: false }),
|
|
684
|
-
publishDate: field.date(),
|
|
685
|
-
contentType: field.select({ options: ['news', 'tutorial', 'announcement'], default: 'news' }),
|
|
686
|
-
category: field.reference({ entity: 'category', required: false }),
|
|
687
|
-
tags: field.reference({ entity: 'category', cardinality: 'many' }),
|
|
688
|
-
coverImage: field.media({ accept: ['image/*'] }),
|
|
689
|
-
},
|
|
690
|
-
behaviors: [
|
|
691
|
-
behavior.publishable(),
|
|
692
|
-
behavior.auditable(),
|
|
693
|
-
behavior.sluggable('title'),
|
|
694
|
-
behavior.revisionable(),
|
|
695
|
-
],
|
|
696
|
-
scope: 'global',
|
|
697
|
-
access: {
|
|
698
|
-
view: 'public',
|
|
699
|
-
create: 'group.editor',
|
|
700
|
-
update: 'group.editor',
|
|
701
|
-
delete: 'group.admin',
|
|
702
|
-
},
|
|
703
|
-
})
|
|
704
|
-
`),await r((0,i.join)(a,"entities/category.ts"),`import { defineEntity, field } from '@murumets-ee/entity'
|
|
705
|
-
|
|
706
|
-
export const Category = defineEntity({
|
|
707
|
-
name: 'category',
|
|
708
|
-
fields: {
|
|
709
|
-
name: field.text({ required: true, maxLength: 100, indexed: true, translatable: true }),
|
|
710
|
-
slug: field.slug({ from: 'name', unique: true }),
|
|
711
|
-
description: field.text({ maxLength: 500, translatable: true }),
|
|
712
|
-
},
|
|
713
|
-
scope: 'global',
|
|
714
|
-
access: {
|
|
715
|
-
view: 'public',
|
|
716
|
-
create: 'group.editor',
|
|
717
|
-
update: 'group.editor',
|
|
718
|
-
delete: 'group.admin',
|
|
719
|
-
},
|
|
720
|
-
})
|
|
721
|
-
`),await r((0,i.join)(a,"lib/app.ts"),`import { createApp, setApp, type ToolkitApp } from '@murumets-ee/core'
|
|
722
|
-
import config from '../toolkit.config'
|
|
723
|
-
|
|
724
|
-
let appInstance: ToolkitApp | null = null
|
|
725
|
-
|
|
726
|
-
export async function getToolkitApp(): Promise<ToolkitApp> {
|
|
727
|
-
if (!appInstance) {
|
|
728
|
-
console.log('Initializing toolkit app...')
|
|
729
|
-
appInstance = await createApp(config)
|
|
730
|
-
setApp(appInstance)
|
|
731
|
-
console.log('Toolkit app initialized')
|
|
732
|
-
}
|
|
733
|
-
return appInstance
|
|
734
|
-
}
|
|
735
|
-
`),await r((0,i.join)(a,"lib/auth.ts"),`import { getToolkitApp } from './app'
|
|
736
|
-
import { getAuth } from '@murumets-ee/auth'
|
|
737
|
-
|
|
738
|
-
export async function getAuthInstance() {
|
|
739
|
-
await getToolkitApp()
|
|
740
|
-
return getAuth()
|
|
741
|
-
}
|
|
742
|
-
`),await r((0,i.join)(a,"lib/auth-client.ts"),`import { createClient } from '@murumets-ee/auth/client'
|
|
743
|
-
|
|
744
|
-
export const authClient = createClient()
|
|
745
|
-
`),await r((0,i.join)(a,"generated/auth-schema.ts"),`// This file is generated by better-auth CLI.
|
|
746
|
-
// Run: npx @better-auth/cli generate --config auth.config.ts -y
|
|
747
|
-
export {}
|
|
748
|
-
`),await r((0,i.join)(a,"scripts/generate-schema.ts"),`/**
|
|
749
|
-
* Generate Drizzle schemas from entity definitions
|
|
750
|
-
*/
|
|
751
|
-
|
|
752
|
-
import { mkdir, writeFile } from 'node:fs/promises'
|
|
753
|
-
import { join } from 'node:path'
|
|
754
|
-
import { generateSchemaCode, generateTranslationSchemaCode } from '@murumets-ee/entity'
|
|
755
|
-
import config from '../toolkit.config'
|
|
756
|
-
|
|
757
|
-
async function generateSchemas() {
|
|
758
|
-
console.log('Generating Drizzle schemas from entity definitions...')
|
|
759
|
-
|
|
760
|
-
const schemaDir = join(import.meta.dirname, '..', 'generated')
|
|
761
|
-
await mkdir(schemaDir, { recursive: true })
|
|
762
|
-
|
|
763
|
-
const imports: string[] = []
|
|
764
|
-
const schemas: string[] = []
|
|
765
|
-
let hasTranslations = false
|
|
766
|
-
|
|
767
|
-
for (const entity of config.entities) {
|
|
768
|
-
console.log(\` - Generating schema for \${entity.name}\`)
|
|
769
|
-
|
|
770
|
-
const schemaCode = generateSchemaCode(entity)
|
|
771
|
-
schemas.push(\`\\n// \${entity.name} table\`)
|
|
772
|
-
schemas.push(schemaCode)
|
|
773
|
-
|
|
774
|
-
const translationCode = generateTranslationSchemaCode(entity)
|
|
775
|
-
if (translationCode) {
|
|
776
|
-
hasTranslations = true
|
|
777
|
-
schemas.push(\`\\n// \${entity.name} translations\`)
|
|
778
|
-
schemas.push(translationCode)
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
const pgCoreTypes = ['pgTable', 'varchar', 'text', 'boolean', 'timestamp', 'integer', 'doublePrecision', 'jsonb', 'uuid', 'index']
|
|
783
|
-
if (hasTranslations) pgCoreTypes.push('unique')
|
|
784
|
-
const pgCoreImports = \`import { \${pgCoreTypes.join(', ')} } from 'drizzle-orm/pg-core'\\n\`
|
|
785
|
-
imports.push(pgCoreImports)
|
|
786
|
-
|
|
787
|
-
const schemaFile = join(schemaDir, 'schema.ts')
|
|
788
|
-
await writeFile(schemaFile, [...imports, ...schemas].join('\\n'))
|
|
789
|
-
|
|
790
|
-
console.log(\`\\nSchemas written to: \${schemaFile}\`)
|
|
791
|
-
console.log('Run \\\`pnpm db:migrate:generate\\\` to create migrations')
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
generateSchemas().catch(console.error)
|
|
795
|
-
`),await r((0,i.join)(a,"scripts/migrate.ts"),`/**
|
|
796
|
-
* Run pending migrations
|
|
797
|
-
*/
|
|
798
|
-
|
|
799
|
-
import { createDbClient, runMigrations } from '@murumets-ee/db'
|
|
800
|
-
|
|
801
|
-
async function migrate() {
|
|
802
|
-
console.log('Running migrations...')
|
|
803
|
-
|
|
804
|
-
try {
|
|
805
|
-
if (!process.env.DATABASE_URL) {
|
|
806
|
-
throw new Error('DATABASE_URL environment variable is required')
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const db = createDbClient({ url: process.env.DATABASE_URL })
|
|
810
|
-
await runMigrations(db, import.meta.dirname + '/..')
|
|
811
|
-
|
|
812
|
-
console.log('Migrations completed successfully')
|
|
813
|
-
process.exit(0)
|
|
814
|
-
} catch (error) {
|
|
815
|
-
console.error('Migration failed:', error)
|
|
816
|
-
process.exit(1)
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
migrate()
|
|
821
|
-
`),await r((0,i.join)(a,"scripts/reset-db.ts"),`/**
|
|
822
|
-
* Reset database (DROP all tables)
|
|
823
|
-
* WARNING: Only for development
|
|
824
|
-
*/
|
|
825
|
-
|
|
826
|
-
import postgres from 'postgres'
|
|
827
|
-
|
|
828
|
-
async function resetDb() {
|
|
829
|
-
const DATABASE_URL = process.env.DATABASE_URL
|
|
830
|
-
|
|
831
|
-
if (!DATABASE_URL) {
|
|
832
|
-
throw new Error('DATABASE_URL not set')
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
console.warn('WARNING: This will DROP ALL TABLES')
|
|
836
|
-
console.log('Database:', DATABASE_URL.split('@')[1])
|
|
837
|
-
|
|
838
|
-
const sql = postgres(DATABASE_URL)
|
|
839
|
-
|
|
840
|
-
try {
|
|
841
|
-
await sql\`
|
|
842
|
-
DROP SCHEMA public CASCADE;
|
|
843
|
-
CREATE SCHEMA public;
|
|
844
|
-
GRANT ALL ON SCHEMA public TO PUBLIC;
|
|
845
|
-
\`
|
|
846
|
-
|
|
847
|
-
console.log('Database reset complete')
|
|
848
|
-
} finally {
|
|
849
|
-
await sql.end()
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
resetDb().catch(console.error)
|
|
854
|
-
`)}async function oe(e,o){let{name:t}=o,a=(0,i.join)(e,"apps/admin");await(0,L.mkdir)((0,i.join)(e,"apps"),{recursive:!0}),p(`pnpm create next-app@${"16"} ${a} --typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias "@/*"`),await x(a,["app/page.tsx","app/page.module.css","app/fonts","README.md","pnpm-workspace.yaml"]),await h((0,i.join)(a,"package.json"),{dependencies:{[`@${t}/config`]:"workspace:*","@murumets-ee/auth-ui":`^${n.myorgAuthUi}`,"better-auth":`^${n.betterAuth}`,"next-intl":`^${n.nextIntl}`,"next-themes":`^${n.nextThemes}`,"lucide-react":`^${n.lucideReact}`,"react-hook-form":`^${n.reactHookForm}`,"@hookform/resolvers":`^${n.hookformResolvers}`,zod:`^${n.zod}`},devDependencies:{"babel-plugin-react-compiler":n.babelReactCompiler}});let{appendToFile:s}=await Promise.resolve().then(()=>(c(),z));await s((0,i.join)(a,".gitignore"),`
|
|
855
|
-
# Environment
|
|
856
|
-
.env*
|
|
857
|
-
!.env.example
|
|
858
|
-
`),await r((0,i.join)(a,"next.config.ts"),`import type { NextConfig } from 'next'
|
|
859
|
-
import createNextIntlPlugin from 'next-intl/plugin'
|
|
860
|
-
|
|
861
|
-
const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
|
|
862
|
-
|
|
863
|
-
const nextConfig: NextConfig = {
|
|
864
|
-
reactCompiler: true,
|
|
865
|
-
transpilePackages: [
|
|
866
|
-
'@${t}/config',
|
|
867
|
-
'@murumets-ee/core',
|
|
868
|
-
'@murumets-ee/entity',
|
|
869
|
-
'@murumets-ee/db',
|
|
870
|
-
'@murumets-ee/logging',
|
|
871
|
-
'@murumets-ee/auth-ui',
|
|
872
|
-
],
|
|
873
|
-
serverExternalPackages: ['drizzle-orm', 'postgres'],
|
|
874
|
-
experimental: {
|
|
875
|
-
serverActions: {
|
|
876
|
-
bodySizeLimit: '2mb',
|
|
877
|
-
},
|
|
878
|
-
},
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
export default withNextIntl(nextConfig)
|
|
882
|
-
`),await r((0,i.join)(a,"proxy.ts"),`import { NextRequest, NextResponse } from 'next/server'
|
|
883
|
-
import createMiddleware from 'next-intl/middleware'
|
|
884
|
-
import { routing } from './i18n/routing'
|
|
885
|
-
|
|
886
|
-
const intlMiddleware = createMiddleware(routing)
|
|
887
|
-
|
|
888
|
-
const protectedPaths = ['/setup']
|
|
889
|
-
|
|
890
|
-
export function proxy(request: NextRequest) {
|
|
891
|
-
const { pathname } = request.nextUrl
|
|
892
|
-
|
|
893
|
-
const localePattern = new RegExp(\`^/(\${routing.locales.join('|')})\`)
|
|
894
|
-
const pathWithoutLocale = pathname.replace(localePattern, '') || '/'
|
|
895
|
-
|
|
896
|
-
if (protectedPaths.some((p) => pathWithoutLocale.startsWith(p))) {
|
|
897
|
-
const session = request.cookies.get('better-auth.session_token')
|
|
898
|
-
if (!session) {
|
|
899
|
-
const locale = pathname.match(localePattern)?.[1] || routing.defaultLocale
|
|
900
|
-
return NextResponse.redirect(new URL(\`/\${locale}/auth/sign-in\`, request.url))
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
return intlMiddleware(request)
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
export const config = {
|
|
908
|
-
matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)',
|
|
909
|
-
}
|
|
910
|
-
`),await r((0,i.join)(a,"i18n/routing.ts"),`import { defineRouting } from 'next-intl/routing'
|
|
911
|
-
|
|
912
|
-
export const routing = defineRouting({
|
|
913
|
-
locales: ['en'],
|
|
914
|
-
defaultLocale: 'en',
|
|
915
|
-
})
|
|
916
|
-
`),await r((0,i.join)(a,"i18n/request.ts"),`import { getRequestConfig } from 'next-intl/server'
|
|
917
|
-
import { hasLocale } from 'next-intl'
|
|
918
|
-
import { routing } from './routing'
|
|
919
|
-
import { getAuthMessages } from '@murumets-ee/auth-ui/i18n'
|
|
920
|
-
|
|
921
|
-
export default getRequestConfig(async ({ requestLocale }) => {
|
|
922
|
-
const requested = await requestLocale
|
|
923
|
-
const locale = hasLocale(routing.locales, requested)
|
|
924
|
-
? requested
|
|
925
|
-
: routing.defaultLocale
|
|
926
|
-
|
|
927
|
-
const authMessages = await getAuthMessages(locale)
|
|
928
|
-
|
|
929
|
-
return {
|
|
930
|
-
locale,
|
|
931
|
-
messages: {
|
|
932
|
-
...authMessages,
|
|
933
|
-
},
|
|
934
|
-
}
|
|
935
|
-
})
|
|
936
|
-
`),await r((0,i.join)(a,"app/globals.css"),`@import "tailwindcss";
|
|
937
|
-
@source "../node_modules/@murumets-ee/auth-ui/dist";
|
|
938
|
-
|
|
939
|
-
@custom-variant dark (&:where(.dark, .dark *));
|
|
940
|
-
|
|
941
|
-
:root {
|
|
942
|
-
--background: #ffffff;
|
|
943
|
-
--foreground: #171717;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
@theme inline {
|
|
947
|
-
--color-background: var(--background);
|
|
948
|
-
--color-foreground: var(--foreground);
|
|
949
|
-
--font-sans: var(--font-geist-sans);
|
|
950
|
-
--font-mono: var(--font-geist-mono);
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
.dark {
|
|
954
|
-
--background: #0a0a0a;
|
|
955
|
-
--foreground: #ededed;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
body {
|
|
959
|
-
background: var(--background);
|
|
960
|
-
color: var(--foreground);
|
|
961
|
-
font-family: Arial, Helvetica, sans-serif;
|
|
962
|
-
}
|
|
963
|
-
`),await r((0,i.join)(a,"app/theme-provider.tsx"),`'use client'
|
|
964
|
-
|
|
965
|
-
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
|
966
|
-
import type { ReactNode } from 'react'
|
|
967
|
-
|
|
968
|
-
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
969
|
-
return (
|
|
970
|
-
<NextThemesProvider
|
|
971
|
-
attribute="class"
|
|
972
|
-
defaultTheme="system"
|
|
973
|
-
enableSystem
|
|
974
|
-
disableTransitionOnChange
|
|
975
|
-
>
|
|
976
|
-
{children}
|
|
977
|
-
</NextThemesProvider>
|
|
978
|
-
)
|
|
979
|
-
}
|
|
980
|
-
`),await r((0,i.join)(a,"app/theme-toggle.tsx"),`'use client'
|
|
981
|
-
|
|
982
|
-
import { useTheme } from 'next-themes'
|
|
983
|
-
import { useState, useEffect } from 'react'
|
|
984
|
-
import { Sun, Moon } from 'lucide-react'
|
|
985
|
-
|
|
986
|
-
export function ThemeToggle() {
|
|
987
|
-
const [mounted, setMounted] = useState(false)
|
|
988
|
-
const { resolvedTheme, setTheme } = useTheme()
|
|
989
|
-
|
|
990
|
-
useEffect(() => { setMounted(true) }, [])
|
|
991
|
-
|
|
992
|
-
if (!mounted) {
|
|
993
|
-
return <div className="h-8 w-8" />
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
return (
|
|
997
|
-
<button
|
|
998
|
-
type="button"
|
|
999
|
-
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
|
|
1000
|
-
className="rounded-md p-1.5 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-200 cursor-pointer"
|
|
1001
|
-
aria-label={resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
1002
|
-
>
|
|
1003
|
-
{resolvedTheme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
|
|
1004
|
-
</button>
|
|
1005
|
-
)
|
|
1006
|
-
}
|
|
1007
|
-
`),await r((0,i.join)(a,"app/nav-header.tsx"),`'use client'
|
|
1008
|
-
|
|
1009
|
-
import Link from 'next/link'
|
|
1010
|
-
import { usePathname } from 'next/navigation'
|
|
1011
|
-
import { ThemeToggle } from './theme-toggle'
|
|
1012
|
-
|
|
1013
|
-
const links = [
|
|
1014
|
-
{ href: '/', label: 'Home' },
|
|
1015
|
-
{ href: '/auth/sign-in', label: 'Sign In' },
|
|
1016
|
-
{ href: '/setup', label: 'Setup' },
|
|
1017
|
-
]
|
|
1018
|
-
|
|
1019
|
-
export function NavHeader() {
|
|
1020
|
-
const pathname = usePathname()
|
|
1021
|
-
|
|
1022
|
-
function isActive(href: string) {
|
|
1023
|
-
return href === '/' ? pathname === '/' : pathname.startsWith(href)
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
const linkClass = (href: string) =>
|
|
1027
|
-
\`px-3 py-1.5 rounded-md text-sm transition-colors \${
|
|
1028
|
-
isActive(href)
|
|
1029
|
-
? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'
|
|
1030
|
-
: 'text-zinc-500 hover:text-zinc-700 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:text-zinc-200 dark:hover:bg-zinc-800/50'
|
|
1031
|
-
}\`
|
|
1032
|
-
|
|
1033
|
-
return (
|
|
1034
|
-
<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">
|
|
1035
|
-
<nav className="max-w-6xl mx-auto flex items-center gap-1 px-4 h-12">
|
|
1036
|
-
<span className="font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none">
|
|
1037
|
-
${t} Admin
|
|
1038
|
-
</span>
|
|
1039
|
-
{links.map(({ href, label }) => (
|
|
1040
|
-
<Link key={href} href={href} className={linkClass(href)}>
|
|
1041
|
-
{label}
|
|
1042
|
-
</Link>
|
|
1043
|
-
))}
|
|
1044
|
-
<div className="ml-auto">
|
|
1045
|
-
<ThemeToggle />
|
|
1046
|
-
</div>
|
|
1047
|
-
</nav>
|
|
1048
|
-
</header>
|
|
1049
|
-
)
|
|
1050
|
-
}
|
|
1051
|
-
`),await r((0,i.join)(a,"app/layout.tsx"),`import './globals.css'
|
|
1052
|
-
|
|
1053
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
1054
|
-
return children
|
|
1055
|
-
}
|
|
1056
|
-
`),await r((0,i.join)(a,"app/[locale]/layout.tsx"),`import type { Metadata } from 'next'
|
|
1057
|
-
import { Geist, Geist_Mono } from 'next/font/google'
|
|
1058
|
-
import { NextIntlClientProvider } from 'next-intl'
|
|
1059
|
-
import { getMessages } from 'next-intl/server'
|
|
1060
|
-
import { notFound } from 'next/navigation'
|
|
1061
|
-
import { routing } from '@/i18n/routing'
|
|
1062
|
-
import { ThemeProvider } from '../theme-provider'
|
|
1063
|
-
import { NavHeader } from '../nav-header'
|
|
1064
|
-
|
|
1065
|
-
const geistSans = Geist({
|
|
1066
|
-
variable: '--font-geist-sans',
|
|
1067
|
-
subsets: ['latin'],
|
|
1068
|
-
})
|
|
1069
|
-
|
|
1070
|
-
const geistMono = Geist_Mono({
|
|
1071
|
-
variable: '--font-geist-mono',
|
|
1072
|
-
subsets: ['latin'],
|
|
1073
|
-
})
|
|
1074
|
-
|
|
1075
|
-
export const metadata: Metadata = {
|
|
1076
|
-
title: '${t} Admin',
|
|
1077
|
-
description: 'Built with Lumi CMS Toolkit',
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
export default async function LocaleLayout({
|
|
1081
|
-
children,
|
|
1082
|
-
params,
|
|
1083
|
-
}: {
|
|
1084
|
-
children: React.ReactNode
|
|
1085
|
-
params: Promise<{ locale: string }>
|
|
1086
|
-
}) {
|
|
1087
|
-
const { locale } = await params
|
|
1088
|
-
if (!routing.locales.includes(locale as any)) notFound()
|
|
1089
|
-
|
|
1090
|
-
const messages = await getMessages()
|
|
1091
|
-
|
|
1092
|
-
return (
|
|
1093
|
-
<html lang={locale} suppressHydrationWarning>
|
|
1094
|
-
<body className={\`\${geistSans.variable} \${geistMono.variable} antialiased\`}>
|
|
1095
|
-
<ThemeProvider>
|
|
1096
|
-
<NextIntlClientProvider locale={locale} messages={messages}>
|
|
1097
|
-
<NavHeader />
|
|
1098
|
-
{children}
|
|
1099
|
-
</NextIntlClientProvider>
|
|
1100
|
-
</ThemeProvider>
|
|
1101
|
-
</body>
|
|
1102
|
-
</html>
|
|
1103
|
-
)
|
|
1104
|
-
}
|
|
1105
|
-
`),await r((0,i.join)(a,"app/[locale]/page.tsx"),`import { createQueryClient } from '@murumets-ee/core/clients'
|
|
1106
|
-
import { Article, Category } from '@${t}/config/entities'
|
|
1107
|
-
import { getToolkitApp } from '@${t}/config/app'
|
|
1108
|
-
|
|
1109
|
-
export default async function HomePage() {
|
|
1110
|
-
await getToolkitApp()
|
|
1111
|
-
const articles = createQueryClient(Article)
|
|
1112
|
-
const categories = createQueryClient(Category)
|
|
1113
|
-
|
|
1114
|
-
const [articleList, categoryList] = await Promise.all([
|
|
1115
|
-
articles.findMany({ limit: 10 }),
|
|
1116
|
-
categories.findMany({}),
|
|
1117
|
-
])
|
|
1118
|
-
|
|
1119
|
-
return (
|
|
1120
|
-
<div className="min-h-screen p-8">
|
|
1121
|
-
<div className="max-w-4xl mx-auto">
|
|
1122
|
-
<h1 className="text-4xl font-bold mb-8">Welcome to ${t}</h1>
|
|
1123
|
-
|
|
1124
|
-
<section className="mb-12">
|
|
1125
|
-
<h2 className="text-2xl font-semibold mb-4">
|
|
1126
|
-
Categories ({categoryList.length})
|
|
1127
|
-
</h2>
|
|
1128
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
1129
|
-
{categoryList.map((cat) => (
|
|
1130
|
-
<div
|
|
1131
|
-
key={cat.id}
|
|
1132
|
-
className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-4"
|
|
1133
|
-
>
|
|
1134
|
-
<h3 className="font-semibold text-lg">{cat.name}</h3>
|
|
1135
|
-
<p className="text-sm text-zinc-600 dark:text-zinc-400">
|
|
1136
|
-
{cat.description}
|
|
1137
|
-
</p>
|
|
1138
|
-
</div>
|
|
1139
|
-
))}
|
|
1140
|
-
</div>
|
|
1141
|
-
</section>
|
|
1142
|
-
|
|
1143
|
-
<section>
|
|
1144
|
-
<h2 className="text-2xl font-semibold mb-4">
|
|
1145
|
-
Articles ({articleList.length})
|
|
1146
|
-
</h2>
|
|
1147
|
-
{articleList.length === 0 ? (
|
|
1148
|
-
<p className="text-zinc-500">No published articles yet.</p>
|
|
1149
|
-
) : (
|
|
1150
|
-
<div className="space-y-4">
|
|
1151
|
-
{articleList.map((article) => (
|
|
1152
|
-
<div
|
|
1153
|
-
key={article.id}
|
|
1154
|
-
className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-6"
|
|
1155
|
-
>
|
|
1156
|
-
<h3 className="text-xl font-bold">{article.title}</h3>
|
|
1157
|
-
<p className="text-zinc-600 dark:text-zinc-400 mt-2">
|
|
1158
|
-
{article.excerpt}
|
|
1159
|
-
</p>
|
|
1160
|
-
</div>
|
|
1161
|
-
))}
|
|
1162
|
-
</div>
|
|
1163
|
-
)}
|
|
1164
|
-
</section>
|
|
1165
|
-
</div>
|
|
1166
|
-
</div>
|
|
1167
|
-
)
|
|
1168
|
-
}
|
|
1169
|
-
`),await r((0,i.join)(a,"app/api/auth/[...all]/route.ts"),`import { toNextJsHandler } from 'better-auth/next-js'
|
|
1170
|
-
import { getAuthInstance } from '@${t}/config/auth'
|
|
1171
|
-
|
|
1172
|
-
let _handler: ReturnType<typeof toNextJsHandler> | null = null
|
|
1173
|
-
|
|
1174
|
-
async function handler() {
|
|
1175
|
-
if (!_handler) {
|
|
1176
|
-
const auth = await getAuthInstance()
|
|
1177
|
-
_handler = toNextJsHandler(auth)
|
|
1178
|
-
}
|
|
1179
|
-
return _handler
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
export async function GET(req: Request) {
|
|
1183
|
-
return (await handler()).GET(req)
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
export async function POST(req: Request) {
|
|
1187
|
-
return (await handler()).POST(req)
|
|
1188
|
-
}
|
|
1189
|
-
`),await r((0,i.join)(a,"app/[locale]/auth/layout.tsx"),`import { AuthProviders } from './providers'
|
|
1190
|
-
import type { ReactNode } from 'react'
|
|
1191
|
-
|
|
1192
|
-
export default function AuthLayout({ children }: { children: ReactNode }) {
|
|
1193
|
-
return (
|
|
1194
|
-
<AuthProviders>
|
|
1195
|
-
<div className="flex min-h-[calc(100vh-3rem)] items-center justify-center px-4">
|
|
1196
|
-
{children}
|
|
1197
|
-
</div>
|
|
1198
|
-
</AuthProviders>
|
|
1199
|
-
)
|
|
1200
|
-
}
|
|
1201
|
-
`),await r((0,i.join)(a,"app/[locale]/auth/providers.tsx"),`'use client'
|
|
1202
|
-
|
|
1203
|
-
import { AuthUIProvider } from '@murumets-ee/auth-ui'
|
|
1204
|
-
import { authClient } from '@${t}/config/auth-client'
|
|
1205
|
-
import Link from 'next/link'
|
|
1206
|
-
import { useRouter } from 'next/navigation'
|
|
1207
|
-
import { useLocale } from 'next-intl'
|
|
1208
|
-
import type { ReactNode } from 'react'
|
|
1209
|
-
|
|
1210
|
-
export function AuthProviders({ children }: { children: ReactNode }) {
|
|
1211
|
-
const router = useRouter()
|
|
1212
|
-
const locale = useLocale()
|
|
1213
|
-
|
|
1214
|
-
return (
|
|
1215
|
-
<AuthUIProvider
|
|
1216
|
-
authClient={authClient as never}
|
|
1217
|
-
basePath="/auth"
|
|
1218
|
-
redirectTo="/"
|
|
1219
|
-
Link={Link}
|
|
1220
|
-
navigate={(url) => router.push(url)}
|
|
1221
|
-
resetPasswordUrl={\`/\${locale}/auth/reset-password\`}
|
|
1222
|
-
>
|
|
1223
|
-
{children}
|
|
1224
|
-
</AuthUIProvider>
|
|
1225
|
-
)
|
|
1226
|
-
}
|
|
1227
|
-
`),await r((0,i.join)(a,"app/[locale]/auth/sign-in/page.tsx"),`import { SignInForm } from '@murumets-ee/auth-ui'
|
|
1228
|
-
|
|
1229
|
-
export default function SignInPage() {
|
|
1230
|
-
return <SignInForm />
|
|
1231
|
-
}
|
|
1232
|
-
`),await r((0,i.join)(a,"app/[locale]/auth/sign-up/page.tsx"),`import { SignUpForm } from '@murumets-ee/auth-ui'
|
|
1233
|
-
|
|
1234
|
-
export default function SignUpPage() {
|
|
1235
|
-
return <SignUpForm />
|
|
1236
|
-
}
|
|
1237
|
-
`),await r((0,i.join)(a,"app/[locale]/auth/forgot-password/page.tsx"),`import { ForgotPasswordForm } from '@murumets-ee/auth-ui'
|
|
1238
|
-
|
|
1239
|
-
export default function ForgotPasswordPage() {
|
|
1240
|
-
return <ForgotPasswordForm />
|
|
1241
|
-
}
|
|
1242
|
-
`),await r((0,i.join)(a,"app/[locale]/auth/reset-password/page.tsx"),`import { Suspense } from 'react'
|
|
1243
|
-
import { ResetPasswordContent } from './content'
|
|
1244
|
-
|
|
1245
|
-
export default function ResetPasswordPage() {
|
|
1246
|
-
return (
|
|
1247
|
-
<Suspense>
|
|
1248
|
-
<ResetPasswordContent />
|
|
1249
|
-
</Suspense>
|
|
1250
|
-
)
|
|
1251
|
-
}
|
|
1252
|
-
`),await r((0,i.join)(a,"app/[locale]/auth/reset-password/content.tsx"),`'use client'
|
|
1253
|
-
|
|
1254
|
-
import { useSearchParams } from 'next/navigation'
|
|
1255
|
-
import { ResetPasswordForm } from '@murumets-ee/auth-ui'
|
|
1256
|
-
|
|
1257
|
-
export function ResetPasswordContent() {
|
|
1258
|
-
const searchParams = useSearchParams()
|
|
1259
|
-
const token = searchParams.get('token')
|
|
1260
|
-
|
|
1261
|
-
if (!token) {
|
|
1262
|
-
return (
|
|
1263
|
-
<div className="text-center">
|
|
1264
|
-
<h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50">
|
|
1265
|
-
Invalid or expired link
|
|
1266
|
-
</h1>
|
|
1267
|
-
<p className="mt-2 text-zinc-500">
|
|
1268
|
-
Please request a new password reset.
|
|
1269
|
-
</p>
|
|
1270
|
-
</div>
|
|
1271
|
-
)
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
return <ResetPasswordForm token={token} />
|
|
1275
|
-
}
|
|
1276
|
-
`),await r((0,i.join)(a,"app/[locale]/setup/page.tsx"),`import { redirect } from 'next/navigation'
|
|
1277
|
-
import { sql } from 'drizzle-orm'
|
|
1278
|
-
import { getToolkitApp } from '@${t}/config/app'
|
|
1279
|
-
import { SetupForm } from './form'
|
|
1280
|
-
|
|
1281
|
-
export default async function SetupPage() {
|
|
1282
|
-
const app = await getToolkitApp()
|
|
1283
|
-
const result = await app.db.readOnly.execute<{ count: string }>(
|
|
1284
|
-
sql\`SELECT COUNT(*)::text as count FROM "user"\`,
|
|
1285
|
-
)
|
|
1286
|
-
|
|
1287
|
-
if (Number(result[0]?.count) > 0) {
|
|
1288
|
-
redirect('/')
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
return (
|
|
1292
|
-
<div className="max-w-md mx-auto p-8">
|
|
1293
|
-
<h1 className="text-2xl font-bold mb-2">Create Admin</h1>
|
|
1294
|
-
<p className="text-sm text-zinc-500 dark:text-zinc-400 mb-6">
|
|
1295
|
-
No users found. Create the first admin account.
|
|
1296
|
-
</p>
|
|
1297
|
-
<SetupForm />
|
|
1298
|
-
</div>
|
|
1299
|
-
)
|
|
1300
|
-
}
|
|
1301
|
-
`),await r((0,i.join)(a,"app/[locale]/setup/form.tsx"),`'use client'
|
|
1302
|
-
|
|
1303
|
-
import { useState } from 'react'
|
|
1304
|
-
import { createFirstAdmin } from './actions'
|
|
1305
|
-
|
|
1306
|
-
export function SetupForm() {
|
|
1307
|
-
const [result, setResult] = useState<{ ok?: boolean; error?: string; email?: string } | null>(null)
|
|
1308
|
-
const [loading, setLoading] = useState(false)
|
|
1309
|
-
|
|
1310
|
-
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
1311
|
-
e.preventDefault()
|
|
1312
|
-
setLoading(true)
|
|
1313
|
-
setResult(null)
|
|
1314
|
-
const res = await createFirstAdmin(new FormData(e.currentTarget))
|
|
1315
|
-
setResult(res)
|
|
1316
|
-
setLoading(false)
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
if (result?.ok) {
|
|
1320
|
-
return (
|
|
1321
|
-
<div className="space-y-2">
|
|
1322
|
-
<p className="text-green-600 dark:text-green-400">Admin created: {result.email}</p>
|
|
1323
|
-
<a href="/auth/sign-in" className="text-blue-600 dark:text-blue-400 hover:underline">
|
|
1324
|
-
Sign in →
|
|
1325
|
-
</a>
|
|
1326
|
-
</div>
|
|
1327
|
-
)
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
return (
|
|
1331
|
-
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
1332
|
-
<label className="text-sm font-medium">
|
|
1333
|
-
Name
|
|
1334
|
-
<input
|
|
1335
|
-
name="name"
|
|
1336
|
-
required
|
|
1337
|
-
className="mt-1 block w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
1338
|
-
/>
|
|
1339
|
-
</label>
|
|
1340
|
-
<label className="text-sm font-medium">
|
|
1341
|
-
Email
|
|
1342
|
-
<input
|
|
1343
|
-
name="email"
|
|
1344
|
-
type="email"
|
|
1345
|
-
required
|
|
1346
|
-
className="mt-1 block w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
1347
|
-
/>
|
|
1348
|
-
</label>
|
|
1349
|
-
<label className="text-sm font-medium">
|
|
1350
|
-
Password (min 8 chars)
|
|
1351
|
-
<input
|
|
1352
|
-
name="password"
|
|
1353
|
-
type="password"
|
|
1354
|
-
required
|
|
1355
|
-
minLength={8}
|
|
1356
|
-
className="mt-1 block w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
1357
|
-
/>
|
|
1358
|
-
</label>
|
|
1359
|
-
|
|
1360
|
-
{result?.error && <p className="text-red-600 dark:text-red-400 text-sm">{result.error}</p>}
|
|
1361
|
-
|
|
1362
|
-
<button
|
|
1363
|
-
type="submit"
|
|
1364
|
-
disabled={loading}
|
|
1365
|
-
className="px-4 py-2 text-sm rounded-md bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-50 transition-colors"
|
|
1366
|
-
>
|
|
1367
|
-
{loading ? 'Creating...' : 'Create Admin'}
|
|
1368
|
-
</button>
|
|
1369
|
-
</form>
|
|
1370
|
-
)
|
|
1371
|
-
}
|
|
1372
|
-
`),await r((0,i.join)(a,"app/[locale]/setup/actions.ts"),`'use server'
|
|
1373
|
-
|
|
1374
|
-
import { sql } from 'drizzle-orm'
|
|
1375
|
-
import { getToolkitApp } from '@${t}/config/app'
|
|
1376
|
-
import { getAuth } from '@murumets-ee/auth'
|
|
1377
|
-
|
|
1378
|
-
export async function createFirstAdmin(formData: FormData) {
|
|
1379
|
-
const email = formData.get('email') as string
|
|
1380
|
-
const password = formData.get('password') as string
|
|
1381
|
-
const name = formData.get('name') as string
|
|
1382
|
-
|
|
1383
|
-
if (!email || !password || !name) {
|
|
1384
|
-
return { error: 'All fields are required' }
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
if (password.length < 8) {
|
|
1388
|
-
return { error: 'Password must be at least 8 characters' }
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
const app = await getToolkitApp()
|
|
1392
|
-
|
|
1393
|
-
const canCreate = await app.db.readWrite.transaction(async (tx) => {
|
|
1394
|
-
await tx.execute(sql\`SELECT pg_advisory_xact_lock(1)\`)
|
|
1395
|
-
|
|
1396
|
-
const result = await tx.execute<{ count: string }>(
|
|
1397
|
-
sql\`SELECT COUNT(*)::text as count FROM "user"\`,
|
|
1398
|
-
)
|
|
1399
|
-
return Number(result[0]?.count) === 0
|
|
1400
|
-
})
|
|
1401
|
-
|
|
1402
|
-
if (!canCreate) {
|
|
1403
|
-
return { error: 'Admin user already exists. Setup is complete.' }
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
const auth = getAuth()
|
|
1407
|
-
|
|
1408
|
-
const adminUser = await auth.api.signUpEmail({
|
|
1409
|
-
body: { email, password, name },
|
|
1410
|
-
})
|
|
1411
|
-
|
|
1412
|
-
await app.db.readWrite.execute(
|
|
1413
|
-
sql\`UPDATE "user" SET role = 'admin' WHERE id = \${adminUser.user.id}\`,
|
|
1414
|
-
)
|
|
1415
|
-
|
|
1416
|
-
return { ok: true, email: adminUser.user.email }
|
|
1417
|
-
}
|
|
1418
|
-
`)}async function ae(e,o){let{name:t}=o,a=(0,i.join)(e,"apps/web");p(`pnpm create next-app@${"16"} ${a} --typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias "@/*"`),await x(a,["app/page.tsx","app/page.module.css","app/fonts","README.md","pnpm-workspace.yaml"]),await h((0,i.join)(a,"package.json"),{dependencies:{[`@${t}/config`]:"workspace:*","@murumets-ee/core":`^${n.myorgCore}`,"@murumets-ee/auth-ui":`^${n.myorgAuthUi}`,"next-intl":`^${n.nextIntl}`,"next-themes":`^${n.nextThemes}`,"lucide-react":`^${n.lucideReact}`},devDependencies:{"babel-plugin-react-compiler":n.babelReactCompiler}});let{appendToFile:s}=await Promise.resolve().then(()=>(c(),z));await s((0,i.join)(a,".gitignore"),`
|
|
1419
|
-
# Environment
|
|
1420
|
-
.env*
|
|
1421
|
-
!.env.example
|
|
1422
|
-
`),await r((0,i.join)(a,"next.config.ts"),`import type { NextConfig } from 'next'
|
|
1423
|
-
import createNextIntlPlugin from 'next-intl/plugin'
|
|
1424
|
-
|
|
1425
|
-
const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
|
|
1426
|
-
|
|
1427
|
-
const nextConfig: NextConfig = {
|
|
1428
|
-
reactCompiler: true,
|
|
1429
|
-
transpilePackages: [
|
|
1430
|
-
'@${t}/config',
|
|
1431
|
-
'@murumets-ee/core',
|
|
1432
|
-
'@murumets-ee/entity',
|
|
1433
|
-
'@murumets-ee/db',
|
|
1434
|
-
'@murumets-ee/logging',
|
|
1435
|
-
'@murumets-ee/auth-ui',
|
|
1436
|
-
],
|
|
1437
|
-
serverExternalPackages: ['drizzle-orm', 'postgres'],
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
export default withNextIntl(nextConfig)
|
|
1441
|
-
`),await r((0,i.join)(a,"proxy.ts"),`import createMiddleware from 'next-intl/middleware'
|
|
1442
|
-
import { routing } from './i18n/routing'
|
|
1443
|
-
|
|
1444
|
-
export default createMiddleware(routing)
|
|
1445
|
-
|
|
1446
|
-
export const config = {
|
|
1447
|
-
matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)',
|
|
1448
|
-
}
|
|
1449
|
-
`),await r((0,i.join)(a,"i18n/routing.ts"),`import { defineRouting } from 'next-intl/routing'
|
|
1450
|
-
|
|
1451
|
-
export const routing = defineRouting({
|
|
1452
|
-
locales: ['en'],
|
|
1453
|
-
defaultLocale: 'en',
|
|
1454
|
-
})
|
|
1455
|
-
`),await r((0,i.join)(a,"i18n/request.ts"),`import { getRequestConfig } from 'next-intl/server'
|
|
1456
|
-
import { hasLocale } from 'next-intl'
|
|
1457
|
-
import { routing } from './routing'
|
|
1458
|
-
import { getAuthMessages } from '@murumets-ee/auth-ui/i18n'
|
|
1459
|
-
|
|
1460
|
-
export default getRequestConfig(async ({ requestLocale }) => {
|
|
1461
|
-
const requested = await requestLocale
|
|
1462
|
-
const locale = hasLocale(routing.locales, requested)
|
|
1463
|
-
? requested
|
|
1464
|
-
: routing.defaultLocale
|
|
1465
|
-
|
|
1466
|
-
const authMessages = await getAuthMessages(locale)
|
|
1467
|
-
|
|
1468
|
-
return {
|
|
1469
|
-
locale,
|
|
1470
|
-
messages: {
|
|
1471
|
-
...authMessages,
|
|
1472
|
-
},
|
|
1473
|
-
}
|
|
1474
|
-
})
|
|
1475
|
-
`),await r((0,i.join)(a,"app/globals.css"),`@import "tailwindcss";
|
|
1476
|
-
@source "../node_modules/@murumets-ee/auth-ui/dist";
|
|
1477
|
-
|
|
1478
|
-
@custom-variant dark (&:where(.dark, .dark *));
|
|
1479
|
-
|
|
1480
|
-
:root {
|
|
1481
|
-
--background: #ffffff;
|
|
1482
|
-
--foreground: #171717;
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
@theme inline {
|
|
1486
|
-
--color-background: var(--background);
|
|
1487
|
-
--color-foreground: var(--foreground);
|
|
1488
|
-
--font-sans: var(--font-geist-sans);
|
|
1489
|
-
--font-mono: var(--font-geist-mono);
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
.dark {
|
|
1493
|
-
--background: #0a0a0a;
|
|
1494
|
-
--foreground: #ededed;
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
body {
|
|
1498
|
-
background: var(--background);
|
|
1499
|
-
color: var(--foreground);
|
|
1500
|
-
font-family: Arial, Helvetica, sans-serif;
|
|
1501
|
-
}
|
|
1502
|
-
`),await r((0,i.join)(a,"app/theme-provider.tsx"),`'use client'
|
|
1503
|
-
|
|
1504
|
-
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
|
1505
|
-
import type { ReactNode } from 'react'
|
|
1506
|
-
|
|
1507
|
-
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
1508
|
-
return (
|
|
1509
|
-
<NextThemesProvider
|
|
1510
|
-
attribute="class"
|
|
1511
|
-
defaultTheme="system"
|
|
1512
|
-
enableSystem
|
|
1513
|
-
disableTransitionOnChange
|
|
1514
|
-
>
|
|
1515
|
-
{children}
|
|
1516
|
-
</NextThemesProvider>
|
|
1517
|
-
)
|
|
1518
|
-
}
|
|
1519
|
-
`),await r((0,i.join)(a,"app/theme-toggle.tsx"),`'use client'
|
|
1520
|
-
|
|
1521
|
-
import { useTheme } from 'next-themes'
|
|
1522
|
-
import { useState, useEffect } from 'react'
|
|
1523
|
-
import { Sun, Moon } from 'lucide-react'
|
|
1524
|
-
|
|
1525
|
-
export function ThemeToggle() {
|
|
1526
|
-
const [mounted, setMounted] = useState(false)
|
|
1527
|
-
const { resolvedTheme, setTheme } = useTheme()
|
|
1528
|
-
|
|
1529
|
-
useEffect(() => { setMounted(true) }, [])
|
|
1530
|
-
|
|
1531
|
-
if (!mounted) {
|
|
1532
|
-
return <div className="h-8 w-8" />
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
return (
|
|
1536
|
-
<button
|
|
1537
|
-
type="button"
|
|
1538
|
-
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
|
|
1539
|
-
className="rounded-md p-1.5 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-200 cursor-pointer"
|
|
1540
|
-
aria-label={resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
1541
|
-
>
|
|
1542
|
-
{resolvedTheme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
|
|
1543
|
-
</button>
|
|
1544
|
-
)
|
|
1545
|
-
}
|
|
1546
|
-
`),await r((0,i.join)(a,"app/nav-header.tsx"),`'use client'
|
|
1547
|
-
|
|
1548
|
-
import Link from 'next/link'
|
|
1549
|
-
import { usePathname } from 'next/navigation'
|
|
1550
|
-
import { ThemeToggle } from './theme-toggle'
|
|
1551
|
-
|
|
1552
|
-
const links = [
|
|
1553
|
-
{ href: '/', label: 'Home' },
|
|
1554
|
-
]
|
|
1555
|
-
|
|
1556
|
-
export function NavHeader() {
|
|
1557
|
-
const pathname = usePathname()
|
|
1558
|
-
|
|
1559
|
-
function isActive(href: string) {
|
|
1560
|
-
return href === '/' ? pathname === '/' : pathname.startsWith(href)
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
const linkClass = (href: string) =>
|
|
1564
|
-
\`px-3 py-1.5 rounded-md text-sm transition-colors \${
|
|
1565
|
-
isActive(href)
|
|
1566
|
-
? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'
|
|
1567
|
-
: 'text-zinc-500 hover:text-zinc-700 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:text-zinc-200 dark:hover:bg-zinc-800/50'
|
|
1568
|
-
}\`
|
|
1569
|
-
|
|
1570
|
-
return (
|
|
1571
|
-
<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">
|
|
1572
|
-
<nav className="max-w-6xl mx-auto flex items-center gap-1 px-4 h-12">
|
|
1573
|
-
<span className="font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none">
|
|
1574
|
-
${t}
|
|
1575
|
-
</span>
|
|
1576
|
-
{links.map(({ href, label }) => (
|
|
1577
|
-
<Link key={href} href={href} className={linkClass(href)}>
|
|
1578
|
-
{label}
|
|
1579
|
-
</Link>
|
|
1580
|
-
))}
|
|
1581
|
-
<div className="ml-auto">
|
|
1582
|
-
<ThemeToggle />
|
|
1583
|
-
</div>
|
|
1584
|
-
</nav>
|
|
1585
|
-
</header>
|
|
1586
|
-
)
|
|
1587
|
-
}
|
|
1588
|
-
`),await r((0,i.join)(a,"app/layout.tsx"),`import './globals.css'
|
|
1589
|
-
|
|
1590
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
1591
|
-
return children
|
|
1592
|
-
}
|
|
1593
|
-
`),await r((0,i.join)(a,"app/[locale]/layout.tsx"),`import type { Metadata } from 'next'
|
|
1594
|
-
import { Geist, Geist_Mono } from 'next/font/google'
|
|
1595
|
-
import { NextIntlClientProvider } from 'next-intl'
|
|
1596
|
-
import { getMessages } from 'next-intl/server'
|
|
1597
|
-
import { notFound } from 'next/navigation'
|
|
1598
|
-
import { routing } from '@/i18n/routing'
|
|
1599
|
-
import { ThemeProvider } from '../theme-provider'
|
|
1600
|
-
import { NavHeader } from '../nav-header'
|
|
1601
|
-
|
|
1602
|
-
const geistSans = Geist({
|
|
1603
|
-
variable: '--font-geist-sans',
|
|
1604
|
-
subsets: ['latin'],
|
|
1605
|
-
})
|
|
1606
|
-
|
|
1607
|
-
const geistMono = Geist_Mono({
|
|
1608
|
-
variable: '--font-geist-mono',
|
|
1609
|
-
subsets: ['latin'],
|
|
1610
|
-
})
|
|
1611
|
-
|
|
1612
|
-
export const metadata: Metadata = {
|
|
1613
|
-
title: '${t}',
|
|
1614
|
-
description: 'Built with Lumi CMS Toolkit',
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
export default async function LocaleLayout({
|
|
1618
|
-
children,
|
|
1619
|
-
params,
|
|
1620
|
-
}: {
|
|
1621
|
-
children: React.ReactNode
|
|
1622
|
-
params: Promise<{ locale: string }>
|
|
1623
|
-
}) {
|
|
1624
|
-
const { locale } = await params
|
|
1625
|
-
if (!routing.locales.includes(locale as any)) notFound()
|
|
1626
|
-
|
|
1627
|
-
const messages = await getMessages()
|
|
1628
|
-
|
|
1629
|
-
return (
|
|
1630
|
-
<html lang={locale} suppressHydrationWarning>
|
|
1631
|
-
<body className={\`\${geistSans.variable} \${geistMono.variable} antialiased\`}>
|
|
1632
|
-
<ThemeProvider>
|
|
1633
|
-
<NextIntlClientProvider locale={locale} messages={messages}>
|
|
1634
|
-
<NavHeader />
|
|
1635
|
-
{children}
|
|
1636
|
-
</NextIntlClientProvider>
|
|
1637
|
-
</ThemeProvider>
|
|
1638
|
-
</body>
|
|
1639
|
-
</html>
|
|
1640
|
-
)
|
|
1641
|
-
}
|
|
1642
|
-
`),await r((0,i.join)(a,"app/[locale]/page.tsx"),`import { createQueryClient } from '@murumets-ee/core/clients'
|
|
1643
|
-
import { Article, Category } from '@${t}/config/entities'
|
|
1644
|
-
import { getToolkitApp } from '@${t}/config/app'
|
|
1645
|
-
|
|
1646
|
-
export default async function HomePage() {
|
|
1647
|
-
await getToolkitApp()
|
|
1648
|
-
const articles = createQueryClient(Article)
|
|
1649
|
-
const categories = createQueryClient(Category)
|
|
1650
|
-
|
|
1651
|
-
const [articleList, categoryList] = await Promise.all([
|
|
1652
|
-
articles.findMany({ limit: 10 }),
|
|
1653
|
-
categories.findMany({}),
|
|
1654
|
-
])
|
|
1655
|
-
|
|
1656
|
-
return (
|
|
1657
|
-
<div className="min-h-screen p-8">
|
|
1658
|
-
<div className="max-w-4xl mx-auto">
|
|
1659
|
-
<h1 className="text-4xl font-bold mb-8">Welcome to ${t}</h1>
|
|
1660
|
-
|
|
1661
|
-
<section className="mb-12">
|
|
1662
|
-
<h2 className="text-2xl font-semibold mb-4">
|
|
1663
|
-
Categories ({categoryList.length})
|
|
1664
|
-
</h2>
|
|
1665
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
1666
|
-
{categoryList.map((cat) => (
|
|
1667
|
-
<div
|
|
1668
|
-
key={cat.id}
|
|
1669
|
-
className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-4"
|
|
1670
|
-
>
|
|
1671
|
-
<h3 className="font-semibold text-lg">{cat.name}</h3>
|
|
1672
|
-
<p className="text-sm text-zinc-600 dark:text-zinc-400">
|
|
1673
|
-
{cat.description}
|
|
1674
|
-
</p>
|
|
1675
|
-
</div>
|
|
1676
|
-
))}
|
|
1677
|
-
</div>
|
|
1678
|
-
</section>
|
|
1679
|
-
|
|
1680
|
-
<section>
|
|
1681
|
-
<h2 className="text-2xl font-semibold mb-4">
|
|
1682
|
-
Articles ({articleList.length})
|
|
1683
|
-
</h2>
|
|
1684
|
-
{articleList.length === 0 ? (
|
|
1685
|
-
<p className="text-zinc-500">No published articles yet.</p>
|
|
1686
|
-
) : (
|
|
1687
|
-
<div className="space-y-4">
|
|
1688
|
-
{articleList.map((article) => (
|
|
1689
|
-
<div
|
|
1690
|
-
key={article.id}
|
|
1691
|
-
className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-6"
|
|
1692
|
-
>
|
|
1693
|
-
<h3 className="text-xl font-bold">{article.title}</h3>
|
|
1694
|
-
<p className="text-zinc-600 dark:text-zinc-400 mt-2">
|
|
1695
|
-
{article.excerpt}
|
|
1696
|
-
</p>
|
|
1697
|
-
</div>
|
|
1698
|
-
))}
|
|
1699
|
-
</div>
|
|
1700
|
-
)}
|
|
1701
|
-
</section>
|
|
1702
|
-
</div>
|
|
1703
|
-
</div>
|
|
1704
|
-
)
|
|
1705
|
-
}
|
|
1706
|
-
`)}var b=require("path");c();async function D(e,o){let{name:t}=o;await r((0,b.join)(e,"next.config.ts"),`import type { NextConfig } from 'next'
|
|
1707
|
-
import createNextIntlPlugin from 'next-intl/plugin'
|
|
1708
|
-
|
|
1709
|
-
const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
|
|
1710
|
-
|
|
1711
|
-
const nextConfig: NextConfig = {
|
|
1712
|
-
output: 'standalone',
|
|
1713
|
-
reactCompiler: true,
|
|
1714
|
-
transpilePackages: [
|
|
1715
|
-
'@murumets-ee/core', '@murumets-ee/entity', '@murumets-ee/db',
|
|
1716
|
-
'@murumets-ee/logging', '@murumets-ee/auth', '@murumets-ee/auth-ui',
|
|
1717
|
-
'@murumets-ee/admin-ui', '@murumets-ee/content', '@murumets-ee/settings',
|
|
1718
|
-
'@murumets-ee/storage', '@murumets-ee/media', '@murumets-ee/taxonomy',
|
|
1719
|
-
'@murumets-ee/queue', '@murumets-ee/mail', '@murumets-ee/ticketing',
|
|
1720
|
-
'@murumets-ee/ticketing-ui', '@murumets-ee/tokens', '@murumets-ee/ui',
|
|
1721
|
-
],
|
|
1722
|
-
serverExternalPackages: ['drizzle-orm', 'postgres', 'pino', 'file-type'],
|
|
1723
|
-
experimental: {
|
|
1724
|
-
serverActions: {
|
|
1725
|
-
bodySizeLimit: '2mb',
|
|
1726
|
-
},
|
|
1727
|
-
},
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
export default withNextIntl(nextConfig)
|
|
1731
|
-
`),await r((0,b.join)(e,"proxy.ts"),`import { NextRequest, NextResponse } from 'next/server'
|
|
1732
|
-
import createMiddleware from 'next-intl/middleware'
|
|
1733
|
-
import { routing } from './i18n/routing'
|
|
1734
|
-
|
|
1735
|
-
const intlMiddleware = createMiddleware(routing)
|
|
1736
|
-
|
|
1737
|
-
const protectedPaths = ['/setup']
|
|
1738
|
-
|
|
1739
|
-
export function proxy(request: NextRequest) {
|
|
1740
|
-
const { pathname } = request.nextUrl
|
|
1741
|
-
|
|
1742
|
-
// Strip locale prefix to check actual path
|
|
1743
|
-
const localePattern = new RegExp(\`^/(\${routing.locales.join('|')})\`)
|
|
1744
|
-
const pathWithoutLocale = pathname.replace(localePattern, '') || '/'
|
|
1745
|
-
|
|
1746
|
-
// Auth check for protected paths
|
|
1747
|
-
if (protectedPaths.some((p) => pathWithoutLocale.startsWith(p))) {
|
|
1748
|
-
const session = request.cookies.get('better-auth.session_token')
|
|
1749
|
-
if (!session) {
|
|
1750
|
-
const locale = pathname.match(localePattern)?.[1] || routing.defaultLocale
|
|
1751
|
-
return NextResponse.redirect(new URL(\`/\${locale}/auth/sign-in\`, request.url))
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
// Locale routing
|
|
1756
|
-
return intlMiddleware(request)
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
export const config = {
|
|
1760
|
-
matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)',
|
|
1761
|
-
}
|
|
1762
|
-
`),await r((0,b.join)(e,"app/layout.tsx"),`import './globals.css'
|
|
1763
|
-
|
|
1764
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
1765
|
-
return children
|
|
1766
|
-
}
|
|
1767
|
-
`),await r((0,b.join)(e,"app/[locale]/layout.tsx"),`import type { Metadata } from 'next'
|
|
1768
|
-
import { Geist, Geist_Mono } from 'next/font/google'
|
|
1769
|
-
import { NextIntlClientProvider } from 'next-intl'
|
|
1770
|
-
import { getMessages } from 'next-intl/server'
|
|
1771
|
-
import { notFound } from 'next/navigation'
|
|
1772
|
-
import { routing } from '@/i18n/routing'
|
|
1773
|
-
import { ThemeProvider } from '../theme-provider'
|
|
1774
|
-
import { NavHeader } from '../nav-header'
|
|
1775
|
-
|
|
1776
|
-
const geistSans = Geist({
|
|
1777
|
-
variable: '--font-geist-sans',
|
|
1778
|
-
subsets: ['latin'],
|
|
1779
|
-
})
|
|
1780
|
-
|
|
1781
|
-
const geistMono = Geist_Mono({
|
|
1782
|
-
variable: '--font-geist-mono',
|
|
1783
|
-
subsets: ['latin'],
|
|
1784
|
-
})
|
|
1785
|
-
|
|
1786
|
-
export const metadata: Metadata = {
|
|
1787
|
-
title: '${t}',
|
|
1788
|
-
description: 'Built with Lumi CMS Toolkit',
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
export default async function LocaleLayout({
|
|
1792
|
-
children,
|
|
1793
|
-
params,
|
|
1794
|
-
}: {
|
|
1795
|
-
children: React.ReactNode
|
|
1796
|
-
params: Promise<{ locale: string }>
|
|
1797
|
-
}) {
|
|
1798
|
-
const { locale } = await params
|
|
1799
|
-
if (!routing.locales.includes(locale as any)) notFound()
|
|
1800
|
-
|
|
1801
|
-
const messages = await getMessages()
|
|
1802
|
-
|
|
1803
|
-
return (
|
|
1804
|
-
<html lang={locale} suppressHydrationWarning>
|
|
1805
|
-
<body className={\`\${geistSans.variable} \${geistMono.variable} antialiased\`}>
|
|
1806
|
-
<ThemeProvider>
|
|
1807
|
-
<NextIntlClientProvider locale={locale} messages={messages}>
|
|
1808
|
-
<NavHeader />
|
|
1809
|
-
{children}
|
|
1810
|
-
</NextIntlClientProvider>
|
|
1811
|
-
</ThemeProvider>
|
|
1812
|
-
</body>
|
|
1813
|
-
</html>
|
|
1814
|
-
)
|
|
1815
|
-
}
|
|
1816
|
-
`),await r((0,b.join)(e,"app/[locale]/page.tsx"),`import { createQueryClient } from '@murumets-ee/core/clients'
|
|
1817
|
-
import { Article, Category } from '@/entities'
|
|
1818
|
-
import { getToolkitApp } from '@/lib/app'
|
|
1819
|
-
|
|
1820
|
-
export default async function HomePage() {
|
|
1821
|
-
await getToolkitApp()
|
|
1822
|
-
const articles = createQueryClient(Article)
|
|
1823
|
-
const categories = createQueryClient(Category)
|
|
1824
|
-
|
|
1825
|
-
const [articleList, categoryList] = await Promise.all([
|
|
1826
|
-
articles.findMany({ limit: 10 }),
|
|
1827
|
-
categories.findMany({}),
|
|
1828
|
-
])
|
|
1829
|
-
|
|
1830
|
-
return (
|
|
1831
|
-
<div className="min-h-screen p-8">
|
|
1832
|
-
<div className="max-w-4xl mx-auto">
|
|
1833
|
-
<h1 className="text-4xl font-bold mb-8">Welcome to ${t}</h1>
|
|
1834
|
-
|
|
1835
|
-
<section className="mb-12">
|
|
1836
|
-
<h2 className="text-2xl font-semibold mb-4">
|
|
1837
|
-
Categories ({categoryList.length})
|
|
1838
|
-
</h2>
|
|
1839
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
1840
|
-
{categoryList.map((cat) => (
|
|
1841
|
-
<div
|
|
1842
|
-
key={cat.id}
|
|
1843
|
-
className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-4"
|
|
1844
|
-
>
|
|
1845
|
-
<h3 className="font-semibold text-lg">{cat.name}</h3>
|
|
1846
|
-
<p className="text-sm text-zinc-600 dark:text-zinc-400">
|
|
1847
|
-
{cat.description}
|
|
1848
|
-
</p>
|
|
1849
|
-
</div>
|
|
1850
|
-
))}
|
|
1851
|
-
</div>
|
|
1852
|
-
</section>
|
|
1853
|
-
|
|
1854
|
-
<section>
|
|
1855
|
-
<h2 className="text-2xl font-semibold mb-4">
|
|
1856
|
-
Articles ({articleList.length})
|
|
1857
|
-
</h2>
|
|
1858
|
-
{articleList.length === 0 ? (
|
|
1859
|
-
<p className="text-zinc-500">No published articles yet.</p>
|
|
1860
|
-
) : (
|
|
1861
|
-
<div className="space-y-4">
|
|
1862
|
-
{articleList.map((article) => (
|
|
1863
|
-
<div
|
|
1864
|
-
key={article.id}
|
|
1865
|
-
className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-6"
|
|
1866
|
-
>
|
|
1867
|
-
<h3 className="text-xl font-bold">{article.title}</h3>
|
|
1868
|
-
<p className="text-zinc-600 dark:text-zinc-400 mt-2">
|
|
1869
|
-
{article.excerpt}
|
|
1870
|
-
</p>
|
|
1871
|
-
</div>
|
|
1872
|
-
))}
|
|
1873
|
-
</div>
|
|
1874
|
-
)}
|
|
1875
|
-
</section>
|
|
1876
|
-
</div>
|
|
1877
|
-
</div>
|
|
1878
|
-
)
|
|
1879
|
-
}
|
|
1880
|
-
`)}var T=require("path");c();async function B(e,o){await r((0,T.join)(e,"scripts/generate-schema.ts"),`import { execSync } from 'node:child_process'
|
|
1881
|
-
execSync('npx @murumets-ee/cli generate', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })
|
|
1882
|
-
`),await r((0,T.join)(e,"scripts/migrate.ts"),`import { execSync } from 'node:child_process'
|
|
1883
|
-
execSync('npx @murumets-ee/cli migrate', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })
|
|
1884
|
-
`),await r((0,T.join)(e,"scripts/reset-db.ts"),`import { execSync } from 'node:child_process'
|
|
1885
|
-
execSync('npx @murumets-ee/cli reset --force', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })
|
|
1886
|
-
`)}var y=require("path");c();async function H(e,o){let{name:t}=o;await r((0,y.join)(e,"app/globals.css"),`@import "tailwindcss";
|
|
1887
|
-
@source "../node_modules/@murumets-ee/auth-ui/dist";
|
|
1888
|
-
|
|
1889
|
-
@custom-variant dark (&:where(.dark, .dark *));
|
|
1890
|
-
|
|
1891
|
-
:root {
|
|
1892
|
-
--background: #ffffff;
|
|
1893
|
-
--foreground: #171717;
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
@theme inline {
|
|
1897
|
-
--color-background: var(--background);
|
|
1898
|
-
--color-foreground: var(--foreground);
|
|
1899
|
-
--font-sans: var(--font-geist-sans);
|
|
1900
|
-
--font-mono: var(--font-geist-mono);
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
.dark {
|
|
1904
|
-
--background: #0a0a0a;
|
|
1905
|
-
--foreground: #ededed;
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
body {
|
|
1909
|
-
background: var(--background);
|
|
1910
|
-
color: var(--foreground);
|
|
1911
|
-
font-family: Arial, Helvetica, sans-serif;
|
|
1912
|
-
}
|
|
1913
|
-
`),await r((0,y.join)(e,"app/theme-provider.tsx"),`'use client'
|
|
1914
|
-
|
|
1915
|
-
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
|
1916
|
-
import type { ReactNode } from 'react'
|
|
1917
|
-
|
|
1918
|
-
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
1919
|
-
return (
|
|
1920
|
-
<NextThemesProvider
|
|
1921
|
-
attribute="class"
|
|
1922
|
-
defaultTheme="system"
|
|
1923
|
-
enableSystem
|
|
1924
|
-
disableTransitionOnChange
|
|
1925
|
-
>
|
|
1926
|
-
{children}
|
|
1927
|
-
</NextThemesProvider>
|
|
1928
|
-
)
|
|
1929
|
-
}
|
|
1930
|
-
`),await r((0,y.join)(e,"app/theme-toggle.tsx"),`'use client'
|
|
1931
|
-
|
|
1932
|
-
import { useTheme } from 'next-themes'
|
|
1933
|
-
import { useState, useEffect } from 'react'
|
|
1934
|
-
import { Sun, Moon } from 'lucide-react'
|
|
1935
|
-
|
|
1936
|
-
export function ThemeToggle() {
|
|
1937
|
-
const [mounted, setMounted] = useState(false)
|
|
1938
|
-
const { resolvedTheme, setTheme } = useTheme()
|
|
1939
|
-
|
|
1940
|
-
useEffect(() => { setMounted(true) }, [])
|
|
1941
|
-
|
|
1942
|
-
if (!mounted) {
|
|
1943
|
-
return <div className="h-8 w-8" />
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
return (
|
|
1947
|
-
<button
|
|
1948
|
-
type="button"
|
|
1949
|
-
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
|
|
1950
|
-
className="rounded-md p-1.5 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-200 cursor-pointer"
|
|
1951
|
-
aria-label={resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
1952
|
-
>
|
|
1953
|
-
{resolvedTheme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
|
|
1954
|
-
</button>
|
|
1955
|
-
)
|
|
1956
|
-
}
|
|
1957
|
-
`),await r((0,y.join)(e,"app/nav-header.tsx"),`'use client'
|
|
1958
|
-
|
|
1959
|
-
import Link from 'next/link'
|
|
1960
|
-
import { usePathname } from 'next/navigation'
|
|
1961
|
-
import { ThemeToggle } from './theme-toggle'
|
|
1962
|
-
|
|
1963
|
-
const links = [
|
|
1964
|
-
{ href: '/', label: 'Home' },
|
|
1965
|
-
{ href: '/auth/sign-in', label: 'Sign In' },
|
|
1966
|
-
{ href: '/setup', label: 'Setup' },
|
|
1967
|
-
]
|
|
1968
|
-
|
|
1969
|
-
export function NavHeader() {
|
|
1970
|
-
const pathname = usePathname()
|
|
1971
|
-
|
|
1972
|
-
function isActive(href: string) {
|
|
1973
|
-
return href === '/' ? pathname === '/' : pathname.startsWith(href)
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
const linkClass = (href: string) =>
|
|
1977
|
-
\`px-3 py-1.5 rounded-md text-sm transition-colors \${
|
|
1978
|
-
isActive(href)
|
|
1979
|
-
? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'
|
|
1980
|
-
: 'text-zinc-500 hover:text-zinc-700 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:text-zinc-200 dark:hover:bg-zinc-800/50'
|
|
1981
|
-
}\`
|
|
1982
|
-
|
|
1983
|
-
return (
|
|
1984
|
-
<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">
|
|
1985
|
-
<nav className="max-w-6xl mx-auto flex items-center gap-1 px-4 h-12">
|
|
1986
|
-
<span className="font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none">
|
|
1987
|
-
${t}
|
|
1988
|
-
</span>
|
|
1989
|
-
{links.map(({ href, label }) => (
|
|
1990
|
-
<Link key={href} href={href} className={linkClass(href)}>
|
|
1991
|
-
{label}
|
|
1992
|
-
</Link>
|
|
1993
|
-
))}
|
|
1994
|
-
<div className="ml-auto">
|
|
1995
|
-
<ThemeToggle />
|
|
1996
|
-
</div>
|
|
1997
|
-
</nav>
|
|
1998
|
-
</header>
|
|
1999
|
-
)
|
|
2000
|
-
}
|
|
2001
|
-
`)}var v=require("path");c();async function W(e,o){let{name:t}=o;await r((0,v.join)(e,"toolkit.config.ts"),`import { auth } from '@murumets-ee/auth/plugin'
|
|
2002
|
-
import { content } from '@murumets-ee/content/plugin'
|
|
2003
|
-
import { defineConfig } from '@murumets-ee/core'
|
|
2004
|
-
import { logging } from '@murumets-ee/logging/plugin'
|
|
2005
|
-
import { mail, ResendMailProvider } from '@murumets-ee/mail'
|
|
2006
|
-
import { media } from '@murumets-ee/media/plugin'
|
|
2007
|
-
import { queue } from '@murumets-ee/queue/plugin'
|
|
2008
|
-
import { settings } from '@murumets-ee/settings/plugin'
|
|
2009
|
-
import { storage } from '@murumets-ee/storage/plugin'
|
|
2010
|
-
import { taxonomy } from '@murumets-ee/taxonomy/plugin'
|
|
2011
|
-
import { ticketing } from '@murumets-ee/ticketing/plugin'
|
|
2012
|
-
import { Article, Category } from './entities'
|
|
2013
|
-
import * as authSchema from './generated/auth-schema'
|
|
2014
|
-
|
|
2015
|
-
if (!process.env.DATABASE_URL) {
|
|
2016
|
-
throw new Error('DATABASE_URL environment variable is required')
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
export default defineConfig({
|
|
2020
|
-
db: {
|
|
2021
|
-
url: process.env.DATABASE_URL,
|
|
2022
|
-
poolMin: 2,
|
|
2023
|
-
poolMax: 10,
|
|
2024
|
-
},
|
|
2025
|
-
logging: {
|
|
2026
|
-
level: (process.env.LOG_LEVEL || 'info') as 'debug' | 'info' | 'warn' | 'error',
|
|
2027
|
-
name: '${t}',
|
|
2028
|
-
},
|
|
2029
|
-
entities: [Category, Article],
|
|
2030
|
-
plugins: [
|
|
2031
|
-
auth({ providers: ['email'], schema: authSchema }),
|
|
2032
|
-
content({
|
|
2033
|
-
locales: [{ code: 'en', label: 'English' }],
|
|
2034
|
-
defaultLocale: 'en',
|
|
2035
|
-
}),
|
|
2036
|
-
logging(),
|
|
2037
|
-
settings(),
|
|
2038
|
-
storage(),
|
|
2039
|
-
media(),
|
|
2040
|
-
taxonomy(),
|
|
2041
|
-
queue(),
|
|
2042
|
-
mail({
|
|
2043
|
-
provider: process.env.RESEND_API_KEY
|
|
2044
|
-
? new ResendMailProvider({ apiKey: process.env.RESEND_API_KEY })
|
|
2045
|
-
: undefined,
|
|
2046
|
-
defaultFrom: process.env.MAIL_FROM ?? 'noreply@example.com',
|
|
2047
|
-
webhookSecret: process.env.RESEND_WEBHOOK_SECRET,
|
|
2048
|
-
}),
|
|
2049
|
-
ticketing({
|
|
2050
|
-
csatSecret: process.env.CSAT_SECRET,
|
|
2051
|
-
}),
|
|
2052
|
-
],
|
|
2053
|
-
projectRoot: import.meta.dirname,
|
|
2054
|
-
})
|
|
2055
|
-
`),await r((0,v.join)(e,"lib/app.ts"),`import { createApp, setApp, type ToolkitApp } from '@murumets-ee/core'
|
|
2056
|
-
import config from '../toolkit.config'
|
|
2057
|
-
|
|
2058
|
-
let appInstance: ToolkitApp | null = null
|
|
2059
|
-
|
|
2060
|
-
export async function getToolkitApp(): Promise<ToolkitApp> {
|
|
2061
|
-
if (!appInstance) {
|
|
2062
|
-
console.log('Initializing toolkit app...')
|
|
2063
|
-
appInstance = await createApp(config)
|
|
2064
|
-
setApp(appInstance)
|
|
2065
|
-
console.log('Toolkit app initialized')
|
|
2066
|
-
}
|
|
2067
|
-
return appInstance
|
|
2068
|
-
}
|
|
2069
|
-
`),await r((0,v.join)(e,"drizzle.config.ts"),`import type { Config } from 'drizzle-kit'
|
|
2070
|
-
|
|
2071
|
-
if (!process.env.DATABASE_URL) {
|
|
2072
|
-
throw new Error('DATABASE_URL environment variable is required')
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
export default {
|
|
2076
|
-
schema: ['./generated/schema.ts', './generated/auth-schema.ts'],
|
|
2077
|
-
out: './migrations',
|
|
2078
|
-
dialect: 'postgresql',
|
|
2079
|
-
dbCredentials: {
|
|
2080
|
-
url: process.env.DATABASE_URL,
|
|
2081
|
-
},
|
|
2082
|
-
} satisfies Config
|
|
2083
|
-
`),await r((0,v.join)(e,"auth.config.ts"),`import { betterAuth } from 'better-auth'
|
|
2084
|
-
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
|
2085
|
-
import { admin } from 'better-auth/plugins'
|
|
2086
|
-
import { organization } from 'better-auth/plugins/organization'
|
|
2087
|
-
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
2088
|
-
import postgres from 'postgres'
|
|
2089
|
-
|
|
2090
|
-
const sql = postgres(process.env.DATABASE_URL!)
|
|
2091
|
-
const db = drizzle(sql)
|
|
2092
|
-
|
|
2093
|
-
export const auth = betterAuth({
|
|
2094
|
-
database: drizzleAdapter(db, { provider: 'pg' }),
|
|
2095
|
-
emailAndPassword: { enabled: true },
|
|
2096
|
-
plugins: [
|
|
2097
|
-
admin(),
|
|
2098
|
-
organization(),
|
|
2099
|
-
],
|
|
2100
|
-
})
|
|
2101
|
-
`),await r((0,v.join)(e,"generated/auth-schema.ts"),`// This file is generated by better-auth CLI.
|
|
2102
|
-
// Run: npx @better-auth/cli generate --config auth.config.ts -y
|
|
2103
|
-
export {}
|
|
2104
|
-
`)}c();async function j(e,o){e.mode==="single"?await ie(e,o):await ne(e,o)}async function ie(e,o){let t=(0,S.join)(process.cwd(),e.name);o?.("Creating Next.js app..."),p(`pnpm create next-app@${"16"} ${e.name} --typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias "@/*"`),await x(t,["app/page.tsx","app/page.module.css","app/fonts","README.md"]),await h((0,S.join)(t,"package.json"),{type:"module",dependencies:{"@murumets-ee/core":`^${n.myorgCore}`,"@murumets-ee/db":`^${n.myorgDb}`,"@murumets-ee/entity":`^${n.myorgEntity}`,"@murumets-ee/logging":`^${n.myorgLogging}`,"@murumets-ee/auth":`^${n.myorgAuth}`,"@murumets-ee/auth-ui":`^${n.myorgAuthUi}`,"@murumets-ee/admin-ui":`^${n.myorgAdminUi}`,"@murumets-ee/content":`^${n.myorgContent}`,"@murumets-ee/settings":`^${n.myorgSettings}`,"@murumets-ee/storage":`^${n.myorgStorage}`,"@murumets-ee/media":`^${n.myorgMedia}`,"@murumets-ee/taxonomy":`^${n.myorgTaxonomy}`,"@murumets-ee/queue":`^${n.myorgQueue}`,"@murumets-ee/mail":`^${n.myorgMail}`,"@murumets-ee/ticketing":`^${n.myorgTicketing}`,"@murumets-ee/ticketing-ui":`^${n.myorgTicketingUi}`,"@murumets-ee/tokens":`^${n.myorgTokens}`,"better-auth":`^${n.betterAuth}`,"drizzle-orm":`^${n.drizzleOrm}`,"next-intl":`^${n.nextIntl}`,"next-themes":`^${n.nextThemes}`,"lucide-react":`^${n.lucideReact}`,postgres:`^${n.postgres}`,"react-hook-form":`^${n.reactHookForm}`,"@hookform/resolvers":`^${n.hookformResolvers}`,zod:`^${n.zod}`},devDependencies:{"drizzle-kit":`^${n.drizzleKit}`,tsx:`^${n.tsx}`,"babel-plugin-react-compiler":n.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"}}),o?.("Adding toolkit files..."),await q(t,e),await I(t,e),await W(t,e),await O(t,e),await F(t,e),await D(t,e),await H(t,e),await B(t,e),e.installDeps&&(o?.("Installing dependencies..."),p("pnpm install",{cwd:t}))}async function ne(e,o){let t=(0,S.join)(process.cwd(),e.name);o?.("Creating workspace..."),await U(t,e,o),e.installDeps&&(o?.("Installing dependencies..."),p("pnpm install",{cwd:t}))}async function se(){let e=process.argv[2],o=await $(e);if(!o)return;let t=d.spinner();try{t.start("Creating Next.js app..."),await j(o,s=>{t.stop(`${w.default.green("\u2713")} Done`),t.start(s)}),t.stop(`${w.default.green("\u2713")} Done`);let a=[`cd ${o.name}`,"cp .env.example .env","docker compose up -d","npx @murumets-ee/cli setup","pnpm db:migrate","pnpm dev","# Visit /setup to create your first admin user"];d.note(a.join(`
|
|
2105
|
-
`),"Next steps"),d.outro(`${w.default.green("Success!")} Your project is ready.`)}catch(a){t.stop(w.default.red("Failed.")),d.log.error(a instanceof Error?a.message:String(a)),process.exit(1)}}se();
|