create-kyro 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -12
- package/dist/index.js +62 -651
- package/package.json +1 -1
- package/src/generators/astro.ts +7 -70
- package/src/generators/config.ts +39 -38
- package/src/generators/files.ts +12 -507
- package/src/generators/packagejson.ts +6 -29
- package/src/index.ts +5 -5
- package/src/prompts.ts +0 -56
- package/test/generators.test.ts +17 -83
- package/test/validators.test.ts +1 -2
- package/bun.lock +0 -296
package/dist/index.js
CHANGED
|
@@ -64,58 +64,6 @@ async function promptUser() {
|
|
|
64
64
|
}
|
|
65
65
|
]
|
|
66
66
|
},
|
|
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
67
|
{
|
|
120
68
|
type: "select",
|
|
121
69
|
name: "template",
|
|
@@ -205,44 +153,25 @@ ${cyan("?")} ${bold(msg)}`);
|
|
|
205
153
|
};
|
|
206
154
|
|
|
207
155
|
// src/generators/packagejson.ts
|
|
208
|
-
function generatePackageJson(answers
|
|
156
|
+
function generatePackageJson(answers) {
|
|
209
157
|
const deps = {
|
|
158
|
+
"astro": "^6.3.1",
|
|
210
159
|
"@kyro-cms/core": "latest",
|
|
211
|
-
|
|
160
|
+
"@kyro-cms/admin": "latest"
|
|
212
161
|
};
|
|
213
162
|
const devDeps = {
|
|
214
|
-
typescript: "^5.7.3"
|
|
163
|
+
"typescript": "^5.7.3"
|
|
215
164
|
};
|
|
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
165
|
if (answers.database === "postgres") {
|
|
226
166
|
deps["pg"] = "^8.13.1";
|
|
227
|
-
deps["@types/pg"] = "^8.11.0";
|
|
228
167
|
} else if (answers.database === "mysql") {
|
|
229
168
|
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
169
|
}
|
|
238
170
|
const scripts = {
|
|
239
|
-
dev: "astro dev",
|
|
240
|
-
build: "astro build",
|
|
241
|
-
preview: "astro preview"
|
|
171
|
+
"dev": "astro dev",
|
|
172
|
+
"build": "astro build",
|
|
173
|
+
"preview": "astro preview"
|
|
242
174
|
};
|
|
243
|
-
if (answers.auth) {
|
|
244
|
-
scripts["db:bootstrap"] = "kyro auth bootstrap";
|
|
245
|
-
}
|
|
246
175
|
if (answers.database === "sqlite") {
|
|
247
176
|
scripts["db:generate"] = "kyro generate";
|
|
248
177
|
scripts["db:push"] = "kyro push";
|
|
@@ -265,58 +194,49 @@ function formatPackageJson(pkg) {
|
|
|
265
194
|
// src/generators/config.ts
|
|
266
195
|
function generateKyroConfig(answers) {
|
|
267
196
|
const imports = ["import { defineConfig } from '@kyro-cms/core';"];
|
|
268
|
-
const adapterLines = [];
|
|
269
197
|
if (answers.database === "sqlite") {
|
|
270
198
|
imports.push("import { createLocalAdapter } from '@kyro-cms/core';");
|
|
271
|
-
adapterLines.push(` adapter: createLocalAdapter({ path: './data.db' }),`);
|
|
272
199
|
} else if (answers.database === "postgres") {
|
|
273
200
|
imports.push("import { createDrizzleAdapter } from '@kyro-cms/core';");
|
|
274
|
-
adapterLines.push(` adapter: createDrizzleAdapter({`);
|
|
275
|
-
adapterLines.push(` connectionString: process.env.DATABASE_URL,`);
|
|
276
|
-
adapterLines.push(` }),`);
|
|
277
201
|
} else if (answers.database === "mysql") {
|
|
278
202
|
imports.push("import { createDrizzleAdapter } from '@kyro-cms/core';");
|
|
203
|
+
} else if (answers.database === "mongodb") {
|
|
204
|
+
imports.push("import { createMongoDBAdapter } from '@kyro-cms/core';");
|
|
205
|
+
}
|
|
206
|
+
const adapterLines = [];
|
|
207
|
+
if (answers.database === "sqlite") {
|
|
208
|
+
adapterLines.push(` adapter: createLocalAdapter({ path: './data.db' }),`);
|
|
209
|
+
} else if (answers.database === "postgres" || answers.database === "mysql") {
|
|
279
210
|
adapterLines.push(` adapter: createDrizzleAdapter({`);
|
|
280
211
|
adapterLines.push(` connectionString: process.env.DATABASE_URL,`);
|
|
281
212
|
adapterLines.push(` }),`);
|
|
282
213
|
} else if (answers.database === "mongodb") {
|
|
283
|
-
imports.push("import { createMongoDBAdapter } from '@kyro-cms/core';");
|
|
284
214
|
adapterLines.push(` adapter: createMongoDBAdapter({`);
|
|
285
215
|
adapterLines.push(` connectionString: process.env.MONGODB_URI,`);
|
|
286
216
|
adapterLines.push(` }),`);
|
|
287
217
|
}
|
|
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
218
|
let templateCollections = "";
|
|
302
219
|
let templateGlobals = "";
|
|
303
220
|
switch (answers.template) {
|
|
304
221
|
case "minimal":
|
|
305
|
-
templateCollections = "import { minimalCollections } from '@kyro-cms/core';";
|
|
222
|
+
templateCollections = "import { minimalCollections } from '@kyro-cms/core/templates';";
|
|
223
|
+
templateGlobals = "import { coreSettingsGlobals } from '@kyro-cms/core/templates';";
|
|
306
224
|
break;
|
|
307
225
|
case "blog":
|
|
308
|
-
templateCollections = "import { blogCollections } from '@kyro-cms/core';";
|
|
226
|
+
templateCollections = "import { blogCollections } from '@kyro-cms/core/templates';";
|
|
227
|
+
templateGlobals = "import { allSettingsGlobals } from '@kyro-cms/core/templates';";
|
|
309
228
|
break;
|
|
310
229
|
case "ecommerce":
|
|
311
|
-
templateCollections = "import { ecommerceCollections } from '@kyro-cms/core';";
|
|
230
|
+
templateCollections = "import { ecommerceCollections } from '@kyro-cms/core/templates';";
|
|
231
|
+
templateGlobals = "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
|
|
312
232
|
break;
|
|
313
233
|
case "kitchen-sink":
|
|
314
|
-
templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core';`;
|
|
234
|
+
templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core/templates';`;
|
|
235
|
+
templateGlobals = "import { allSettingsGlobals, ecommerceSettingsGlobals } from '@kyro-cms/core/templates';";
|
|
315
236
|
break;
|
|
316
237
|
}
|
|
317
|
-
if (templateCollections)
|
|
318
|
-
|
|
319
|
-
}
|
|
238
|
+
if (templateCollections) imports.push(templateCollections);
|
|
239
|
+
if (templateGlobals) imports.push(templateGlobals);
|
|
320
240
|
let collectionsConfig = "";
|
|
321
241
|
if (answers.template === "minimal") {
|
|
322
242
|
collectionsConfig = ` collections: Object.values(minimalCollections),`;
|
|
@@ -332,89 +252,43 @@ function generateKyroConfig(answers) {
|
|
|
332
252
|
...Object.values(kitchenSinkCollections),
|
|
333
253
|
],`;
|
|
334
254
|
}
|
|
335
|
-
|
|
255
|
+
let globalsConfig = "";
|
|
256
|
+
if (answers.template === "minimal") {
|
|
257
|
+
globalsConfig = ` globals: coreSettingsGlobals,`;
|
|
258
|
+
} else if (answers.template === "blog" || answers.template === "ecommerce") {
|
|
259
|
+
globalsConfig = answers.template === "ecommerce" ? ` globals: [...allSettingsGlobals, ...ecommerceSettingsGlobals],` : ` globals: allSettingsGlobals,`;
|
|
260
|
+
} else if (answers.template === "kitchen-sink") {
|
|
261
|
+
globalsConfig = ` globals: [...allSettingsGlobals, ...ecommerceSettingsGlobals],`;
|
|
262
|
+
}
|
|
263
|
+
return `${imports.join("\n")}
|
|
336
264
|
|
|
337
265
|
export default defineConfig({
|
|
338
266
|
name: '${answers.projectName}',
|
|
339
|
-
prefix: '/api'
|
|
340
|
-
${
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
},
|
|
267
|
+
prefix: '/api',
|
|
268
|
+
${adapterLines.join("\n")}
|
|
269
|
+
${collectionsConfig}
|
|
270
|
+
${globalsConfig}
|
|
271
|
+
auth: true,
|
|
345
272
|
});`;
|
|
346
|
-
return config;
|
|
347
273
|
}
|
|
348
274
|
|
|
349
275
|
// src/generators/astro.ts
|
|
350
276
|
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';" : ""}
|
|
277
|
+
return `import { defineConfig } from 'astro/config';
|
|
278
|
+
import kyro from '@kyro-cms/core';
|
|
279
|
+
import { kyroAdmin } from '@kyro-cms/admin';
|
|
398
280
|
|
|
399
281
|
export default defineConfig({
|
|
400
|
-
output: 'server'
|
|
401
|
-
|
|
282
|
+
output: 'server',
|
|
402
283
|
integrations: [
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
plugins: [
|
|
407
|
-
${vitePlugins.join("\n")}
|
|
408
|
-
],${nativeExternals}
|
|
409
|
-
},` : `
|
|
410
|
-
vite: {${nativeExternals}
|
|
411
|
-
},`}
|
|
284
|
+
kyro({ adminPath: '/admin', apiPath: '/api' }),
|
|
285
|
+
kyroAdmin({ basePath: '/admin', apiPath: '/api' }),
|
|
286
|
+
],
|
|
412
287
|
server: {
|
|
413
288
|
port: 4321,
|
|
414
289
|
host: true,
|
|
415
290
|
},
|
|
416
291
|
});`;
|
|
417
|
-
return config;
|
|
418
292
|
}
|
|
419
293
|
|
|
420
294
|
// src/generators/files.ts
|
|
@@ -464,74 +338,26 @@ npm run dev
|
|
|
464
338
|
|
|
465
339
|
Visit [http://localhost:4321/admin](http://localhost:4321/admin) to access the admin.
|
|
466
340
|
|
|
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
|
-
` : ""}
|
|
341
|
+
The first user to register will automatically be granted super admin privileges.
|
|
485
342
|
|
|
486
343
|
## Documentation
|
|
487
344
|
|
|
488
|
-
Visit [https://kyro.
|
|
345
|
+
Visit [https://kyro.dev](https://kyro.dev) for full documentation.
|
|
489
346
|
`;
|
|
490
347
|
writeFileSync(join(projectDir, "README.md"), readme);
|
|
491
348
|
const envExample = `# Kyro CMS Configuration
|
|
349
|
+
# Copy this file to .env and fill in your values
|
|
492
350
|
|
|
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
|
|
494
|
-
|
|
495
|
-
${answers.auth ? `# Authentication (uses SQLite at ./data/auth.db - no Redis needed)
|
|
496
|
-
JWT_SECRET=change-this-to-a-random-32-character-string
|
|
497
|
-
JWT_EXPIRES_IN=24h
|
|
498
|
-
|
|
499
|
-
# Registration control (set to false to disable public registration after first user)
|
|
500
|
-
KYRO_ALLOW_REGISTRATION=true
|
|
351
|
+
${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"}
|
|
501
352
|
|
|
502
|
-
#
|
|
503
|
-
|
|
353
|
+
# JWT secret for authentication tokens
|
|
354
|
+
JWT_SECRET=change-this-to-a-random-64-character-string
|
|
504
355
|
|
|
505
|
-
#
|
|
506
|
-
#
|
|
507
|
-
#
|
|
508
|
-
# SMTP_SECURE=false
|
|
509
|
-
# SMTP_USER=your-email@example.com
|
|
510
|
-
# SMTP_PASS=your-password
|
|
511
|
-
# SMTP_FROM=noreply@example.com` : ""}
|
|
356
|
+
# Admin credentials (used for first-user bootstrap)
|
|
357
|
+
# KYRO_ADMIN_EMAIL=admin@example.com
|
|
358
|
+
# KYRO_ADMIN_PASSWORD=SecurePass123!
|
|
512
359
|
`;
|
|
513
360
|
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
361
|
const indexPage = `---
|
|
536
362
|
const title = "${answers.projectName}";
|
|
537
363
|
---
|
|
@@ -540,13 +366,13 @@ const title = "${answers.projectName}";
|
|
|
540
366
|
<head>
|
|
541
367
|
<meta charset="UTF-8" />
|
|
542
368
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
543
|
-
<title
|
|
369
|
+
<title>{title}</title>
|
|
544
370
|
</head>
|
|
545
371
|
<body>
|
|
546
372
|
<main>
|
|
547
|
-
<h1>Welcome to
|
|
373
|
+
<h1>Welcome to {title}</h1>
|
|
548
374
|
<p>Your Kyro CMS is ready.</p>
|
|
549
|
-
|
|
375
|
+
<p><a href="/admin">Go to Admin Dashboard →</a></p>
|
|
550
376
|
</main>
|
|
551
377
|
</body>
|
|
552
378
|
</html>
|
|
@@ -576,428 +402,13 @@ const title = "${answers.projectName}";
|
|
|
576
402
|
</style>
|
|
577
403
|
`;
|
|
578
404
|
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
405
|
}
|
|
995
406
|
|
|
996
407
|
// src/index.ts
|
|
997
408
|
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
|
|
998
409
|
import { join as join2 } from "path";
|
|
999
410
|
import { execSync } from "child_process";
|
|
1000
|
-
var VERSION = "0.
|
|
411
|
+
var VERSION = "0.4.0";
|
|
1001
412
|
async function main() {
|
|
1002
413
|
logger.intro("create-kyro", VERSION);
|
|
1003
414
|
const answers = await promptUser();
|
|
@@ -1016,7 +427,7 @@ async function main() {
|
|
|
1016
427
|
mkdirSync2(projectDir, { recursive: true });
|
|
1017
428
|
logger.success("Project directory created");
|
|
1018
429
|
logger.step(2, steps.length, steps[1]);
|
|
1019
|
-
const pkg = generatePackageJson(answers
|
|
430
|
+
const pkg = generatePackageJson(answers);
|
|
1020
431
|
writeFileSync2(
|
|
1021
432
|
join2(projectDir, "package.json"),
|
|
1022
433
|
formatPackageJson(pkg)
|
|
@@ -1059,9 +470,9 @@ async function main() {
|
|
|
1059
470
|
console.log(` ${logger ? "\x1B[36m" : ""}npm run dev${logger ? "\x1B[0m" : ""}`);
|
|
1060
471
|
console.log("");
|
|
1061
472
|
console.log(" Visit http://localhost:4321 to see your app.");
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
473
|
+
console.log(" Visit http://localhost:4321/admin for the admin dashboard.");
|
|
474
|
+
console.log("");
|
|
475
|
+
console.log(" The first user to register will be the super admin.");
|
|
1065
476
|
console.log("");
|
|
1066
477
|
}
|
|
1067
478
|
main().catch((error) => {
|