create-kyro 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -13
- package/dist/index.js +67 -664
- package/package.json +1 -1
- package/src/generators/astro.ts +7 -70
- package/src/generators/config.ts +39 -40
- package/src/generators/files.ts +18 -509
- package/src/generators/packagejson.ts +6 -35
- package/src/index.ts +5 -5
- package/src/prompts.ts +1 -62
- package/test/generators.test.ts +13 -100
- package/test/validators.test.ts +0 -1
package/dist/index.js
CHANGED
|
@@ -52,11 +52,6 @@ async function promptUser() {
|
|
|
52
52
|
description: "Recommended for production. Robust and scalable.",
|
|
53
53
|
value: "postgres"
|
|
54
54
|
},
|
|
55
|
-
{
|
|
56
|
-
title: "MySQL",
|
|
57
|
-
description: "Popular choice for web applications.",
|
|
58
|
-
value: "mysql"
|
|
59
|
-
},
|
|
60
55
|
{
|
|
61
56
|
title: "MongoDB",
|
|
62
57
|
description: "Best for flexible, document-based schemas.",
|
|
@@ -64,58 +59,6 @@ async function promptUser() {
|
|
|
64
59
|
}
|
|
65
60
|
]
|
|
66
61
|
},
|
|
67
|
-
{
|
|
68
|
-
type: "select",
|
|
69
|
-
name: "styling",
|
|
70
|
-
message: "Styling:",
|
|
71
|
-
hint: " ",
|
|
72
|
-
choices: [
|
|
73
|
-
{
|
|
74
|
-
title: "Tailwind CSS",
|
|
75
|
-
description: "Utility-first CSS framework, excellent DX",
|
|
76
|
-
value: "tailwind"
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
title: "CSS Modules",
|
|
80
|
-
description: "Scoped CSS, no extra dependencies",
|
|
81
|
-
value: "cssmodules"
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
title: "Styled Components",
|
|
85
|
-
description: "CSS-in-JS with tagged template literals",
|
|
86
|
-
value: "styled"
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
title: "None",
|
|
90
|
-
description: "Bring your own styling solution",
|
|
91
|
-
value: "none"
|
|
92
|
-
}
|
|
93
|
-
]
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
type: "toggle",
|
|
97
|
-
name: "auth",
|
|
98
|
-
message: "Add authentication (JWT)?",
|
|
99
|
-
initial: true,
|
|
100
|
-
active: "Yes",
|
|
101
|
-
inactive: "No"
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
type: "toggle",
|
|
105
|
-
name: "versioning",
|
|
106
|
-
message: "Add versioning/drafts?",
|
|
107
|
-
initial: true,
|
|
108
|
-
active: "Yes",
|
|
109
|
-
inactive: "No"
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
type: "toggle",
|
|
113
|
-
name: "admin",
|
|
114
|
-
message: "Include admin dashboard?",
|
|
115
|
-
initial: true,
|
|
116
|
-
active: "Yes",
|
|
117
|
-
inactive: "No"
|
|
118
|
-
},
|
|
119
62
|
{
|
|
120
63
|
type: "select",
|
|
121
64
|
name: "template",
|
|
@@ -205,44 +148,20 @@ ${cyan("?")} ${bold(msg)}`);
|
|
|
205
148
|
};
|
|
206
149
|
|
|
207
150
|
// src/generators/packagejson.ts
|
|
208
|
-
function generatePackageJson(answers
|
|
151
|
+
function generatePackageJson(answers) {
|
|
209
152
|
const deps = {
|
|
153
|
+
"astro": "^6.3.1",
|
|
210
154
|
"@kyro-cms/core": "latest",
|
|
211
|
-
|
|
155
|
+
"@kyro-cms/admin": "latest"
|
|
212
156
|
};
|
|
213
157
|
const devDeps = {
|
|
214
|
-
typescript: "^5.7.3"
|
|
158
|
+
"typescript": "^5.7.3"
|
|
215
159
|
};
|
|
216
|
-
if (answers.styling === "tailwind") {
|
|
217
|
-
deps["@astrojs/react"] = "^4.2.0";
|
|
218
|
-
deps["react"] = "^19.0.0";
|
|
219
|
-
deps["react-dom"] = "^19.0.0";
|
|
220
|
-
deps["tailwindcss"] = "^4.0.0";
|
|
221
|
-
deps["@tailwindcss/vite"] = "^4.0.0";
|
|
222
|
-
devDeps["@types/react"] = "^19.0.0";
|
|
223
|
-
devDeps["@types/react-dom"] = "^19.0.0";
|
|
224
|
-
}
|
|
225
|
-
if (answers.database === "postgres") {
|
|
226
|
-
deps["pg"] = "^8.13.1";
|
|
227
|
-
deps["@types/pg"] = "^8.11.0";
|
|
228
|
-
} else if (answers.database === "mysql") {
|
|
229
|
-
deps["mysql2"] = "^3.12.0";
|
|
230
|
-
} else if (answers.database === "mongodb") {
|
|
231
|
-
deps["mongodb"] = "^6.12.0";
|
|
232
|
-
}
|
|
233
|
-
if (answers.admin) {
|
|
234
|
-
deps["@kyro-cms/admin"] = "latest";
|
|
235
|
-
deps["@astrojs/node"] = "^9.5.5";
|
|
236
|
-
deps["lucide-react"] = "^0.475.0";
|
|
237
|
-
}
|
|
238
160
|
const scripts = {
|
|
239
|
-
dev: "astro dev",
|
|
240
|
-
build: "astro build",
|
|
241
|
-
preview: "astro preview"
|
|
161
|
+
"dev": "astro dev",
|
|
162
|
+
"build": "astro build",
|
|
163
|
+
"preview": "astro preview"
|
|
242
164
|
};
|
|
243
|
-
if (answers.auth) {
|
|
244
|
-
scripts["db:bootstrap"] = "kyro auth bootstrap";
|
|
245
|
-
}
|
|
246
165
|
if (answers.database === "sqlite") {
|
|
247
166
|
scripts["db:generate"] = "kyro generate";
|
|
248
167
|
scripts["db:push"] = "kyro push";
|
|
@@ -265,58 +184,47 @@ function formatPackageJson(pkg) {
|
|
|
265
184
|
// src/generators/config.ts
|
|
266
185
|
function generateKyroConfig(answers) {
|
|
267
186
|
const imports = ["import { defineConfig } from '@kyro-cms/core';"];
|
|
268
|
-
const adapterLines = [];
|
|
269
187
|
if (answers.database === "sqlite") {
|
|
270
188
|
imports.push("import { createLocalAdapter } from '@kyro-cms/core';");
|
|
271
|
-
adapterLines.push(` adapter: createLocalAdapter({ path: './data.db' }),`);
|
|
272
189
|
} else if (answers.database === "postgres") {
|
|
273
190
|
imports.push("import { createDrizzleAdapter } from '@kyro-cms/core';");
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
191
|
+
} else if (answers.database === "mongodb") {
|
|
192
|
+
imports.push("import { createMongoDBAdapter } from '@kyro-cms/core';");
|
|
193
|
+
}
|
|
194
|
+
const adapterLines = [];
|
|
195
|
+
if (answers.database === "sqlite") {
|
|
196
|
+
adapterLines.push(` adapter: createLocalAdapter({ path: './data.db' }),`);
|
|
197
|
+
} else if (answers.database === "postgres") {
|
|
279
198
|
adapterLines.push(` adapter: createDrizzleAdapter({`);
|
|
280
199
|
adapterLines.push(` connectionString: process.env.DATABASE_URL,`);
|
|
281
200
|
adapterLines.push(` }),`);
|
|
282
201
|
} else if (answers.database === "mongodb") {
|
|
283
|
-
imports.push("import { createMongoDBAdapter } from '@kyro-cms/core';");
|
|
284
202
|
adapterLines.push(` adapter: createMongoDBAdapter({`);
|
|
285
203
|
adapterLines.push(` connectionString: process.env.MONGODB_URI,`);
|
|
286
204
|
adapterLines.push(` }),`);
|
|
287
205
|
}
|
|
288
|
-
const apiConfig = [
|
|
289
|
-
" rest: true,",
|
|
290
|
-
" graphql: true,",
|
|
291
|
-
" trpc: true,",
|
|
292
|
-
" websocket: true,"
|
|
293
|
-
];
|
|
294
|
-
const features = [];
|
|
295
|
-
if (answers.auth) {
|
|
296
|
-
features.push(" auth: true,");
|
|
297
|
-
}
|
|
298
|
-
if (answers.versioning) {
|
|
299
|
-
features.push(" versioning: true,");
|
|
300
|
-
}
|
|
301
206
|
let templateCollections = "";
|
|
302
207
|
let templateGlobals = "";
|
|
303
208
|
switch (answers.template) {
|
|
304
209
|
case "minimal":
|
|
305
|
-
templateCollections = "import { minimalCollections } from '@kyro-cms/core';";
|
|
210
|
+
templateCollections = "import { minimalCollections } from '@kyro-cms/core/templates';";
|
|
211
|
+
templateGlobals = "import { coreSettingsGlobals } from '@kyro-cms/core/templates';";
|
|
306
212
|
break;
|
|
307
213
|
case "blog":
|
|
308
|
-
templateCollections = "import { blogCollections } from '@kyro-cms/core';";
|
|
214
|
+
templateCollections = "import { blogCollections } from '@kyro-cms/core/templates';";
|
|
215
|
+
templateGlobals = "import { allSettingsGlobals } from '@kyro-cms/core/templates';";
|
|
309
216
|
break;
|
|
310
217
|
case "ecommerce":
|
|
311
|
-
templateCollections = "import { ecommerceCollections } from '@kyro-cms/core';";
|
|
218
|
+
templateCollections = "import { ecommerceCollections } from '@kyro-cms/core/templates';";
|
|
219
|
+
templateGlobals = "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
|
|
312
220
|
break;
|
|
313
221
|
case "kitchen-sink":
|
|
314
|
-
templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core';`;
|
|
222
|
+
templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core/templates';`;
|
|
223
|
+
templateGlobals = "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
|
|
315
224
|
break;
|
|
316
225
|
}
|
|
317
|
-
if (templateCollections)
|
|
318
|
-
|
|
319
|
-
}
|
|
226
|
+
if (templateCollections) imports.push(templateCollections);
|
|
227
|
+
if (templateGlobals) imports.push(templateGlobals);
|
|
320
228
|
let collectionsConfig = "";
|
|
321
229
|
if (answers.template === "minimal") {
|
|
322
230
|
collectionsConfig = ` collections: Object.values(minimalCollections),`;
|
|
@@ -332,89 +240,43 @@ function generateKyroConfig(answers) {
|
|
|
332
240
|
...Object.values(kitchenSinkCollections),
|
|
333
241
|
],`;
|
|
334
242
|
}
|
|
335
|
-
|
|
243
|
+
let globalsConfig = "";
|
|
244
|
+
if (answers.template === "minimal") {
|
|
245
|
+
globalsConfig = ` globals: coreSettingsGlobals,`;
|
|
246
|
+
} else if (answers.template === "blog" || answers.template === "ecommerce") {
|
|
247
|
+
globalsConfig = answers.template === "ecommerce" ? ` globals: [...allSettingsGlobals, ...ecommerceSettingsGlobals],` : ` globals: allSettingsGlobals,`;
|
|
248
|
+
} else if (answers.template === "kitchen-sink") {
|
|
249
|
+
globalsConfig = ` globals: [...allSettingsGlobals, ...ecommerceSettingsGlobals],`;
|
|
250
|
+
}
|
|
251
|
+
return `${imports.join("\n")}
|
|
336
252
|
|
|
337
253
|
export default defineConfig({
|
|
338
254
|
name: '${answers.projectName}',
|
|
339
|
-
prefix: '/api'
|
|
340
|
-
${
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
},
|
|
255
|
+
prefix: '/api',
|
|
256
|
+
${adapterLines.join("\n")}
|
|
257
|
+
${collectionsConfig}
|
|
258
|
+
${globalsConfig}
|
|
259
|
+
auth: true,
|
|
345
260
|
});`;
|
|
346
|
-
return config;
|
|
347
261
|
}
|
|
348
262
|
|
|
349
263
|
// src/generators/astro.ts
|
|
350
264
|
function generateAstroConfig(answers) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (answers.styling === "tailwind") {
|
|
355
|
-
integrations.push(" react(),");
|
|
356
|
-
vitePlugins.push(" tailwindcss(),");
|
|
357
|
-
}
|
|
358
|
-
const adapter = answers.admin ? `
|
|
359
|
-
adapter: node({
|
|
360
|
-
mode: 'standalone'
|
|
361
|
-
}),` : "";
|
|
362
|
-
const nativeExternals = `
|
|
363
|
-
ssr: {
|
|
364
|
-
external: [
|
|
365
|
-
'better-sqlite3',
|
|
366
|
-
'sharp',
|
|
367
|
-
'ssh2',
|
|
368
|
-
'cpu-features',
|
|
369
|
-
'ssh2-sftp-client',
|
|
370
|
-
'ioredis',
|
|
371
|
-
'nodemailer',
|
|
372
|
-
'jsonwebtoken',
|
|
373
|
-
'@mapbox/node-pre-gyp',
|
|
374
|
-
'mock-aws-s3',
|
|
375
|
-
'aws-sdk',
|
|
376
|
-
'nock',
|
|
377
|
-
],
|
|
378
|
-
},
|
|
379
|
-
optimizeDeps: {
|
|
380
|
-
exclude: [
|
|
381
|
-
'better-sqlite3',
|
|
382
|
-
'sharp',
|
|
383
|
-
'ssh2',
|
|
384
|
-
'cpu-features',
|
|
385
|
-
'ssh2-sftp-client',
|
|
386
|
-
'ioredis',
|
|
387
|
-
'nodemailer',
|
|
388
|
-
'jsonwebtoken',
|
|
389
|
-
'@mapbox/node-pre-gyp',
|
|
390
|
-
'mock-aws-s3',
|
|
391
|
-
'aws-sdk',
|
|
392
|
-
'nock',
|
|
393
|
-
],
|
|
394
|
-
},`;
|
|
395
|
-
const config = `import { defineConfig } from 'astro/config';
|
|
396
|
-
import node from '@astrojs/node';
|
|
397
|
-
${answers.styling === "tailwind" ? "import react from '@astrojs/react';\nimport tailwindcss from '@tailwindcss/vite';" : ""}
|
|
265
|
+
return `import { defineConfig } from 'astro/config';
|
|
266
|
+
import kyro from '@kyro-cms/core';
|
|
267
|
+
import { kyroAdmin } from '@kyro-cms/admin';
|
|
398
268
|
|
|
399
269
|
export default defineConfig({
|
|
400
|
-
output: 'server'
|
|
401
|
-
|
|
270
|
+
output: 'server',
|
|
402
271
|
integrations: [
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
plugins: [
|
|
407
|
-
${vitePlugins.join("\n")}
|
|
408
|
-
],${nativeExternals}
|
|
409
|
-
},` : `
|
|
410
|
-
vite: {${nativeExternals}
|
|
411
|
-
},`}
|
|
272
|
+
kyro({ adminPath: '/admin', apiPath: '/api' }),
|
|
273
|
+
kyroAdmin({ basePath: '/admin', apiPath: '/api' }),
|
|
274
|
+
],
|
|
412
275
|
server: {
|
|
413
276
|
port: 4321,
|
|
414
277
|
host: true,
|
|
415
278
|
},
|
|
416
279
|
});`;
|
|
417
|
-
return config;
|
|
418
280
|
}
|
|
419
281
|
|
|
420
282
|
// src/generators/files.ts
|
|
@@ -427,7 +289,11 @@ function generateProjectFiles(answers, projectDir) {
|
|
|
427
289
|
mkdirSync(pagesDir, { recursive: true });
|
|
428
290
|
mkdirSync(publicDir, { recursive: true });
|
|
429
291
|
if (answers.database === "sqlite") {
|
|
430
|
-
|
|
292
|
+
envDbSection = "# SQLite (local) - no additional config needed";
|
|
293
|
+
} else if (answers.database === "postgres") {
|
|
294
|
+
envDbSection = "# Database connection (PostgreSQL)\nDATABASE_URL=postgresql://user:password@localhost:5432/kyro_cms";
|
|
295
|
+
} else {
|
|
296
|
+
envDbSection = "# MongoDB connection\nMONGODB_URI=mongodb://localhost:27017/kyro_cms";
|
|
431
297
|
}
|
|
432
298
|
const tsconfig = `{
|
|
433
299
|
"extends": "astro/tsconfigs/strict",
|
|
@@ -464,74 +330,26 @@ npm run dev
|
|
|
464
330
|
|
|
465
331
|
Visit [http://localhost:4321/admin](http://localhost:4321/admin) to access the admin.
|
|
466
332
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
Before logging into the admin, you need to create an admin user. Run:
|
|
470
|
-
|
|
471
|
-
\`\`\`bash
|
|
472
|
-
npm run db:bootstrap
|
|
473
|
-
\`\`\`
|
|
474
|
-
|
|
475
|
-
Or set environment variables to auto-bootstrap on startup:
|
|
476
|
-
|
|
477
|
-
\`\`\`bash
|
|
478
|
-
# .env
|
|
479
|
-
KYRO_ADMIN_EMAIL=admin@example.com
|
|
480
|
-
KYRO_ADMIN_PASSWORD=SecurePass123!
|
|
481
|
-
\`\`\`
|
|
482
|
-
|
|
483
|
-
Then restart the dev server.
|
|
484
|
-
` : ""}
|
|
333
|
+
The first user to register will automatically be granted super admin privileges.
|
|
485
334
|
|
|
486
335
|
## Documentation
|
|
487
336
|
|
|
488
|
-
Visit [https://kyro.
|
|
337
|
+
Visit [https://kyro.dev](https://kyro.dev) for full documentation.
|
|
489
338
|
`;
|
|
490
339
|
writeFileSync(join(projectDir, "README.md"), readme);
|
|
491
340
|
const envExample = `# Kyro CMS Configuration
|
|
341
|
+
# Copy this file to .env and fill in your values
|
|
492
342
|
|
|
493
|
-
${answers.database === "sqlite" ? "# SQLite (local) - no additional config needed" : answers.database === "postgres" || answers.database === "mysql" ? "# Database connection (PostgreSQL/MySQL)\nDATABASE_URL=postgresql://user:password@localhost:5432/kyro_cms
|
|
343
|
+
${answers.database === "sqlite" ? "# SQLite (local) - no additional config needed" : answers.database === "postgres" || answers.database === "mysql" ? "# Database connection (PostgreSQL/MySQL)\nDATABASE_URL=postgresql://user:password@localhost:5432/kyro_cms" : "# MongoDB connection\nMONGODB_URI=mongodb://localhost:27017/kyro_cms"}
|
|
494
344
|
|
|
495
|
-
|
|
496
|
-
JWT_SECRET=change-this-to-a-random-
|
|
497
|
-
JWT_EXPIRES_IN=24h
|
|
345
|
+
# JWT secret for authentication tokens
|
|
346
|
+
JWT_SECRET=change-this-to-a-random-64-character-string
|
|
498
347
|
|
|
499
|
-
#
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
# Optional: Custom auth database path (default: ./data/auth.db)
|
|
503
|
-
# KYRO_AUTH_DB_PATH=./data/auth.db
|
|
504
|
-
|
|
505
|
-
# Optional: SMTP for emails
|
|
506
|
-
# SMTP_HOST=smtp.example.com
|
|
507
|
-
# SMTP_PORT=587
|
|
508
|
-
# SMTP_SECURE=false
|
|
509
|
-
# SMTP_USER=your-email@example.com
|
|
510
|
-
# SMTP_PASS=your-password
|
|
511
|
-
# SMTP_FROM=noreply@example.com` : ""}
|
|
348
|
+
# Admin credentials (used for first-user bootstrap)
|
|
349
|
+
# KYRO_ADMIN_EMAIL=admin@example.com
|
|
350
|
+
# KYRO_ADMIN_PASSWORD=SecurePass123!
|
|
512
351
|
`;
|
|
513
352
|
writeFileSync(join(projectDir, ".env.example"), envExample);
|
|
514
|
-
const spec = `# ${answers.projectName}
|
|
515
|
-
|
|
516
|
-
## Overview
|
|
517
|
-
|
|
518
|
-
This project uses Kyro CMS - an Astro-native headless CMS.
|
|
519
|
-
|
|
520
|
-
## Configuration
|
|
521
|
-
|
|
522
|
-
- **Database**: ${answers.database === "sqlite" ? "SQLite (local-first)" : answers.database}
|
|
523
|
-
- **APIs**: REST, GraphQL, tRPC, WebSocket
|
|
524
|
-
- **Styling**: ${answers.styling}
|
|
525
|
-
- **Auth**: ${answers.auth ? "Enabled" : "Disabled"}
|
|
526
|
-
- **Versioning**: ${answers.versioning ? "Enabled" : "Disabled"}
|
|
527
|
-
- **Admin**: ${answers.admin ? "Included" : "Not included"}
|
|
528
|
-
- **Template**: ${answers.template}
|
|
529
|
-
|
|
530
|
-
## Collections
|
|
531
|
-
|
|
532
|
-
${answers.template === "minimal" ? "- Posts" : answers.template === "blog" ? "- Posts\n- Categories\n- Media" : "- Products\n- Categories\n- Customers\n- Orders\n- Coupons"}
|
|
533
|
-
`;
|
|
534
|
-
writeFileSync(join(projectDir, "SPEC.md"), spec);
|
|
535
353
|
const indexPage = `---
|
|
536
354
|
const title = "${answers.projectName}";
|
|
537
355
|
---
|
|
@@ -540,13 +358,13 @@ const title = "${answers.projectName}";
|
|
|
540
358
|
<head>
|
|
541
359
|
<meta charset="UTF-8" />
|
|
542
360
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
543
|
-
<title
|
|
361
|
+
<title>{title}</title>
|
|
544
362
|
</head>
|
|
545
363
|
<body>
|
|
546
364
|
<main>
|
|
547
|
-
<h1>Welcome to
|
|
365
|
+
<h1>Welcome to {title}</h1>
|
|
548
366
|
<p>Your Kyro CMS is ready.</p>
|
|
549
|
-
|
|
367
|
+
<p><a href="/admin">Go to Admin Dashboard →</a></p>
|
|
550
368
|
</main>
|
|
551
369
|
</body>
|
|
552
370
|
</html>
|
|
@@ -576,428 +394,13 @@ const title = "${answers.projectName}";
|
|
|
576
394
|
</style>
|
|
577
395
|
`;
|
|
578
396
|
writeFileSync(join(pagesDir, "index.astro"), indexPage);
|
|
579
|
-
if (answers.admin) {
|
|
580
|
-
const adminDir = join(pagesDir, "admin");
|
|
581
|
-
mkdirSync(adminDir, { recursive: true });
|
|
582
|
-
const adminIndex = `---
|
|
583
|
-
import { Admin } from '@kyro-cms/admin';
|
|
584
|
-
import config from '../../../kyro.config';
|
|
585
|
-
---
|
|
586
|
-
<!DOCTYPE html>
|
|
587
|
-
<html lang="en">
|
|
588
|
-
<head>
|
|
589
|
-
<meta charset="UTF-8" />
|
|
590
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
591
|
-
<title>Admin - ${answers.projectName}</title>
|
|
592
|
-
</head>
|
|
593
|
-
<body>
|
|
594
|
-
<Admin client:load config={config} />
|
|
595
|
-
</body>
|
|
596
|
-
</html>
|
|
597
|
-
`;
|
|
598
|
-
writeFileSync(join(adminDir, "index.astro"), adminIndex);
|
|
599
|
-
}
|
|
600
|
-
if (answers.auth) {
|
|
601
|
-
const authApiDir = join(pagesDir, "api", "auth");
|
|
602
|
-
mkdirSync(authApiDir, { recursive: true });
|
|
603
|
-
writeFileSync(
|
|
604
|
-
join(authApiDir, "login.ts"),
|
|
605
|
-
generateLoginEndpoint(answers.database)
|
|
606
|
-
);
|
|
607
|
-
writeFileSync(
|
|
608
|
-
join(authApiDir, "register.ts"),
|
|
609
|
-
generateRegisterEndpoint(answers.database)
|
|
610
|
-
);
|
|
611
|
-
writeFileSync(
|
|
612
|
-
join(authApiDir, "logout.ts"),
|
|
613
|
-
generateLogoutEndpoint(answers.database)
|
|
614
|
-
);
|
|
615
|
-
writeFileSync(join(authApiDir, "me.ts"), generateMeEndpoint());
|
|
616
|
-
writeFileSync(
|
|
617
|
-
join(authApiDir, "users.ts"),
|
|
618
|
-
generateUsersEndpoint(answers.database)
|
|
619
|
-
);
|
|
620
|
-
writeFileSync(join(srcDir, "middleware.ts"), generateMiddleware());
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
function generateLoginEndpoint(database) {
|
|
624
|
-
return `import type { APIRoute } from "astro";
|
|
625
|
-
import { SQLiteAuthAdapter } from "@kyro-cms/core";
|
|
626
|
-
import jwt from "jsonwebtoken";
|
|
627
|
-
|
|
628
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
629
|
-
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
|
|
630
|
-
|
|
631
|
-
async function getAuthApi() {
|
|
632
|
-
return new SQLiteAuthAdapter({ path: "./data/auth.db" });
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
export const POST: APIRoute = async ({ request }) => {
|
|
636
|
-
try {
|
|
637
|
-
const body = (await request.json()) as {
|
|
638
|
-
email?: string;
|
|
639
|
-
password?: string;
|
|
640
|
-
};
|
|
641
|
-
const { email, password } = body;
|
|
642
|
-
|
|
643
|
-
if (!email || !password) {
|
|
644
|
-
return new Response(
|
|
645
|
-
JSON.stringify({ error: "Email and password required" }),
|
|
646
|
-
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
const adapter = await getAuthApi();
|
|
651
|
-
await adapter.connect();
|
|
652
|
-
|
|
653
|
-
const user = await adapter.findUserByEmail(email);
|
|
654
|
-
if (!user || !user.passwordHash) {
|
|
655
|
-
await adapter.disconnect();
|
|
656
|
-
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
|
657
|
-
status: 401,
|
|
658
|
-
headers: { "Content-Type": "application/json" },
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const valid = await adapter.verifyPassword(password, user.passwordHash);
|
|
663
|
-
if (!valid) {
|
|
664
|
-
await adapter.disconnect();
|
|
665
|
-
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
|
666
|
-
status: 401,
|
|
667
|
-
headers: { "Content-Type": "application/json" },
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const session = await adapter.createSession(user.id, {
|
|
672
|
-
ipAddress: request.headers.get("x-forwarded-for") || "unknown",
|
|
673
|
-
userAgent: request.headers.get("user-agent") || "",
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
const token = jwt.sign(
|
|
677
|
-
{
|
|
678
|
-
sub: user.id,
|
|
679
|
-
email: user.email,
|
|
680
|
-
role: user.role,
|
|
681
|
-
tenantId: user.tenantId,
|
|
682
|
-
},
|
|
683
|
-
JWT_SECRET,
|
|
684
|
-
{ expiresIn: JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
|
|
685
|
-
);
|
|
686
|
-
|
|
687
|
-
await adapter.disconnect();
|
|
688
|
-
|
|
689
|
-
const { passwordHash, ...safeUser } = user;
|
|
690
|
-
|
|
691
|
-
return new Response(
|
|
692
|
-
JSON.stringify({
|
|
693
|
-
success: true,
|
|
694
|
-
user: safeUser,
|
|
695
|
-
token,
|
|
696
|
-
refreshToken: session.refreshToken,
|
|
697
|
-
}),
|
|
698
|
-
{
|
|
699
|
-
status: 200,
|
|
700
|
-
headers: { "Content-Type": "application/json" },
|
|
701
|
-
},
|
|
702
|
-
);
|
|
703
|
-
} catch (error) {
|
|
704
|
-
console.error("Login error:", error);
|
|
705
|
-
return new Response(JSON.stringify({ error: "Login failed" }), {
|
|
706
|
-
status: 500,
|
|
707
|
-
headers: { "Content-Type": "application/json" },
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
};
|
|
711
|
-
`;
|
|
712
|
-
}
|
|
713
|
-
function generateRegisterEndpoint(database) {
|
|
714
|
-
return `import type { APIRoute } from "astro";
|
|
715
|
-
import { SQLiteAuthAdapter } from "@kyro-cms/core";
|
|
716
|
-
import jwt from "jsonwebtoken";
|
|
717
|
-
|
|
718
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
719
|
-
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
|
|
720
|
-
const ALLOW_REGISTRATION = process.env.KYRO_ALLOW_REGISTRATION !== "false";
|
|
721
|
-
|
|
722
|
-
async function getAuthApi() {
|
|
723
|
-
return new SQLiteAuthAdapter({ path: "./data/auth.db" });
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
export const POST: APIRoute = async ({ request }) => {
|
|
727
|
-
try {
|
|
728
|
-
const body = (await request.json()) as {
|
|
729
|
-
email?: string;
|
|
730
|
-
password?: string;
|
|
731
|
-
confirmPassword?: string;
|
|
732
|
-
};
|
|
733
|
-
const { email, password, confirmPassword } = body;
|
|
734
|
-
|
|
735
|
-
if (!email || !password) {
|
|
736
|
-
return new Response(
|
|
737
|
-
JSON.stringify({ error: "Email and password required" }),
|
|
738
|
-
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
if (password !== confirmPassword) {
|
|
743
|
-
return new Response(
|
|
744
|
-
JSON.stringify({ error: "Passwords do not match" }),
|
|
745
|
-
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
746
|
-
);
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
if (password.length < 8) {
|
|
750
|
-
return new Response(
|
|
751
|
-
JSON.stringify({ error: "Password must be at least 8 characters" }),
|
|
752
|
-
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
753
|
-
);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const adapter = await getAuthApi();
|
|
757
|
-
try {
|
|
758
|
-
await adapter.connect();
|
|
759
|
-
} catch {
|
|
760
|
-
return new Response(
|
|
761
|
-
JSON.stringify({ error: "Unable to connect to auth storage." }),
|
|
762
|
-
{ status: 500, headers: { "Content-Type": "application/json" } },
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
const existingUser = await adapter.findUserByEmail(email);
|
|
767
|
-
if (existingUser) {
|
|
768
|
-
await adapter.disconnect();
|
|
769
|
-
return new Response(
|
|
770
|
-
JSON.stringify({ error: "Email already registered" }),
|
|
771
|
-
{ status: 409, headers: { "Content-Type": "application/json" } },
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const isFirstUser = !(await adapter.hasAnyUsers());
|
|
776
|
-
|
|
777
|
-
if (!isFirstUser && !ALLOW_REGISTRATION) {
|
|
778
|
-
await adapter.disconnect();
|
|
779
|
-
return new Response(
|
|
780
|
-
JSON.stringify({ error: "Registration is disabled" }),
|
|
781
|
-
{ status: 403, headers: { "Content-Type": "application/json" } },
|
|
782
|
-
);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
const passwordHash = await adapter.hashPassword(password);
|
|
786
|
-
const user = await adapter.createUser({
|
|
787
|
-
email,
|
|
788
|
-
passwordHash,
|
|
789
|
-
role: isFirstUser ? "super_admin" : "editor",
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
if (isFirstUser) {
|
|
793
|
-
await adapter.updateUser(user.id, { emailVerified: true });
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
const session = await adapter.createSession(user.id, {
|
|
797
|
-
ipAddress: request.headers.get("x-forwarded-for") || "unknown",
|
|
798
|
-
userAgent: request.headers.get("user-agent") || "",
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
const token = jwt.sign(
|
|
802
|
-
{
|
|
803
|
-
sub: user.id,
|
|
804
|
-
email: user.email,
|
|
805
|
-
role: user.role,
|
|
806
|
-
tenantId: user.tenantId,
|
|
807
|
-
},
|
|
808
|
-
JWT_SECRET,
|
|
809
|
-
{ expiresIn: JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
|
|
810
|
-
);
|
|
811
|
-
|
|
812
|
-
await adapter.disconnect();
|
|
813
|
-
|
|
814
|
-
const { passwordHash: _, ...safeUser } = user;
|
|
815
|
-
|
|
816
|
-
return new Response(
|
|
817
|
-
JSON.stringify({
|
|
818
|
-
success: true,
|
|
819
|
-
isFirstUser,
|
|
820
|
-
user: safeUser,
|
|
821
|
-
token,
|
|
822
|
-
refreshToken: session.refreshToken,
|
|
823
|
-
}),
|
|
824
|
-
{
|
|
825
|
-
status: 201,
|
|
826
|
-
headers: { "Content-Type": "application/json" },
|
|
827
|
-
},
|
|
828
|
-
);
|
|
829
|
-
} catch (error) {
|
|
830
|
-
console.error("Registration error:", error);
|
|
831
|
-
return new Response(JSON.stringify({ error: "Registration failed" }), {
|
|
832
|
-
status: 500,
|
|
833
|
-
headers: { "Content-Type": "application/json" },
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
};
|
|
837
|
-
`;
|
|
838
|
-
}
|
|
839
|
-
function generateLogoutEndpoint(database) {
|
|
840
|
-
return `import type { APIRoute } from "astro";
|
|
841
|
-
import { SQLiteAuthAdapter } from "@kyro-cms/core";
|
|
842
|
-
|
|
843
|
-
async function getAuthApi() {
|
|
844
|
-
return new SQLiteAuthAdapter({ path: "./data/auth.db" });
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
export const POST: APIRoute = async ({ request }) => {
|
|
848
|
-
try {
|
|
849
|
-
const authHeader = request.headers.get("authorization");
|
|
850
|
-
const token = authHeader?.startsWith("Bearer ")
|
|
851
|
-
? authHeader.slice(7)
|
|
852
|
-
: null;
|
|
853
|
-
|
|
854
|
-
if (token) {
|
|
855
|
-
const adapter = await getAuthApi();
|
|
856
|
-
await adapter.connect();
|
|
857
|
-
await adapter.deleteSession(token);
|
|
858
|
-
await adapter.disconnect();
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
return new Response(
|
|
862
|
-
JSON.stringify({ success: true }),
|
|
863
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
864
|
-
);
|
|
865
|
-
} catch {
|
|
866
|
-
return new Response(
|
|
867
|
-
JSON.stringify({ success: true }),
|
|
868
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
869
|
-
);
|
|
870
|
-
}
|
|
871
|
-
};
|
|
872
|
-
`;
|
|
873
|
-
}
|
|
874
|
-
function generateMeEndpoint() {
|
|
875
|
-
return `import type { APIRoute } from "astro";
|
|
876
|
-
import jwt from "jsonwebtoken";
|
|
877
|
-
|
|
878
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
879
|
-
|
|
880
|
-
export const GET: APIRoute = async ({ request }) => {
|
|
881
|
-
const authHeader = request.headers.get("authorization");
|
|
882
|
-
const token = authHeader?.startsWith("Bearer ")
|
|
883
|
-
? authHeader.slice(7)
|
|
884
|
-
: null;
|
|
885
|
-
|
|
886
|
-
if (!token) {
|
|
887
|
-
return new Response(
|
|
888
|
-
JSON.stringify({ error: "Not authenticated" }),
|
|
889
|
-
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
890
|
-
);
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
try {
|
|
894
|
-
const payload = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
|
|
895
|
-
return new Response(
|
|
896
|
-
JSON.stringify({
|
|
897
|
-
id: payload.sub,
|
|
898
|
-
email: payload.email,
|
|
899
|
-
role: payload.role,
|
|
900
|
-
}),
|
|
901
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
902
|
-
);
|
|
903
|
-
} catch {
|
|
904
|
-
return new Response(
|
|
905
|
-
JSON.stringify({ error: "Invalid token" }),
|
|
906
|
-
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
907
|
-
);
|
|
908
|
-
}
|
|
909
|
-
};
|
|
910
|
-
`;
|
|
911
|
-
}
|
|
912
|
-
function generateUsersEndpoint(database) {
|
|
913
|
-
return `import type { APIRoute } from "astro";
|
|
914
|
-
import { SQLiteAuthAdapter } from "@kyro-cms/core";
|
|
915
|
-
|
|
916
|
-
async function getAuthApi() {
|
|
917
|
-
return new SQLiteAuthAdapter({ path: "./data/auth.db" });
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
export const GET: APIRoute = async () => {
|
|
921
|
-
try {
|
|
922
|
-
const adapter = await getAuthApi();
|
|
923
|
-
await adapter.connect();
|
|
924
|
-
|
|
925
|
-
const hasUsers = await adapter.hasAnyUsers();
|
|
926
|
-
|
|
927
|
-
await adapter.disconnect();
|
|
928
|
-
|
|
929
|
-
return new Response(JSON.stringify({ hasUsers }), {
|
|
930
|
-
status: 200,
|
|
931
|
-
headers: { "Content-Type": "application/json" },
|
|
932
|
-
});
|
|
933
|
-
} catch {
|
|
934
|
-
return new Response(JSON.stringify({ hasUsers: false }), {
|
|
935
|
-
status: 200,
|
|
936
|
-
headers: { "Content-Type": "application/json" },
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
};
|
|
940
|
-
`;
|
|
941
|
-
}
|
|
942
|
-
function generateMiddleware() {
|
|
943
|
-
return `import type { MiddlewareHandler } from "astro";
|
|
944
|
-
import jwt from "jsonwebtoken";
|
|
945
|
-
|
|
946
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
947
|
-
|
|
948
|
-
const PUBLIC_PATHS = [
|
|
949
|
-
"/api/auth/login",
|
|
950
|
-
"/api/auth/logout",
|
|
951
|
-
"/api/auth/register",
|
|
952
|
-
"/api/auth/me",
|
|
953
|
-
"/api/auth/users",
|
|
954
|
-
"/api/health",
|
|
955
|
-
"/favicon.svg",
|
|
956
|
-
];
|
|
957
|
-
|
|
958
|
-
const PUBLIC_PREFIXES = ["/api/auth/", "/admin"];
|
|
959
|
-
|
|
960
|
-
export const onRequest: MiddlewareHandler = async ({ request, url }, next) => {
|
|
961
|
-
const pathname = new URL(url).pathname;
|
|
962
|
-
|
|
963
|
-
if (PUBLIC_PATHS.includes(pathname) || PUBLIC_PATHS.includes(pathname.replace(/\\/$/, ""))) {
|
|
964
|
-
return next();
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
for (const prefix of PUBLIC_PREFIXES) {
|
|
968
|
-
if (pathname.startsWith(prefix)) {
|
|
969
|
-
return next();
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
const authHeader = request.headers.get("authorization");
|
|
974
|
-
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
975
|
-
|
|
976
|
-
if (!token) {
|
|
977
|
-
return new Response(
|
|
978
|
-
JSON.stringify({ error: "Authentication required" }),
|
|
979
|
-
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
980
|
-
);
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
try {
|
|
984
|
-
jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
|
|
985
|
-
return next();
|
|
986
|
-
} catch {
|
|
987
|
-
return new Response(
|
|
988
|
-
JSON.stringify({ error: "Invalid or expired token" }),
|
|
989
|
-
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
};
|
|
993
|
-
`;
|
|
994
397
|
}
|
|
995
398
|
|
|
996
399
|
// src/index.ts
|
|
997
400
|
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
|
|
998
401
|
import { join as join2 } from "path";
|
|
999
402
|
import { execSync } from "child_process";
|
|
1000
|
-
var VERSION = "0.
|
|
403
|
+
var VERSION = "0.4.0";
|
|
1001
404
|
async function main() {
|
|
1002
405
|
logger.intro("create-kyro", VERSION);
|
|
1003
406
|
const answers = await promptUser();
|
|
@@ -1016,7 +419,7 @@ async function main() {
|
|
|
1016
419
|
mkdirSync2(projectDir, { recursive: true });
|
|
1017
420
|
logger.success("Project directory created");
|
|
1018
421
|
logger.step(2, steps.length, steps[1]);
|
|
1019
|
-
const pkg = generatePackageJson(answers
|
|
422
|
+
const pkg = generatePackageJson(answers);
|
|
1020
423
|
writeFileSync2(
|
|
1021
424
|
join2(projectDir, "package.json"),
|
|
1022
425
|
formatPackageJson(pkg)
|
|
@@ -1059,9 +462,9 @@ async function main() {
|
|
|
1059
462
|
console.log(` ${logger ? "\x1B[36m" : ""}npm run dev${logger ? "\x1B[0m" : ""}`);
|
|
1060
463
|
console.log("");
|
|
1061
464
|
console.log(" Visit http://localhost:4321 to see your app.");
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
465
|
+
console.log(" Visit http://localhost:4321/admin for the admin dashboard.");
|
|
466
|
+
console.log("");
|
|
467
|
+
console.log(" The first user to register will be the super admin.");
|
|
1065
468
|
console.log("");
|
|
1066
469
|
}
|
|
1067
470
|
main().catch((error) => {
|