create-kyro 0.1.2 → 0.1.4
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/index.js +545 -39
- package/package.json +2 -2
- package/my-kyro-app/README.md +0 -14
- package/my-kyro-app/SPEC.md +0 -21
- package/my-kyro-app/astro.config.mjs +0 -23
- package/my-kyro-app/kyro.config.ts +0 -19
- package/my-kyro-app/package.json +0 -30
package/dist/index.js
CHANGED
|
@@ -86,12 +86,14 @@ async function promptUser() {
|
|
|
86
86
|
{
|
|
87
87
|
title: "tRPC",
|
|
88
88
|
description: "End-to-end typesafe APIs, great for TypeScript",
|
|
89
|
-
value: "trpc"
|
|
89
|
+
value: "trpc",
|
|
90
|
+
selected: true
|
|
90
91
|
},
|
|
91
92
|
{
|
|
92
93
|
title: "WebSocket",
|
|
93
94
|
description: "Real-time bidirectional communication",
|
|
94
|
-
value: "websocket"
|
|
95
|
+
value: "websocket",
|
|
96
|
+
selected: true
|
|
95
97
|
}
|
|
96
98
|
]
|
|
97
99
|
},
|
|
@@ -127,6 +129,7 @@ async function promptUser() {
|
|
|
127
129
|
type: "toggle",
|
|
128
130
|
name: "auth",
|
|
129
131
|
message: "Add authentication (JWT)?",
|
|
132
|
+
initial: true,
|
|
130
133
|
active: "Yes",
|
|
131
134
|
inactive: "No"
|
|
132
135
|
},
|
|
@@ -134,6 +137,7 @@ async function promptUser() {
|
|
|
134
137
|
type: "toggle",
|
|
135
138
|
name: "versioning",
|
|
136
139
|
message: "Add versioning/drafts?",
|
|
140
|
+
initial: true,
|
|
137
141
|
active: "Yes",
|
|
138
142
|
inactive: "No"
|
|
139
143
|
},
|
|
@@ -150,6 +154,7 @@ async function promptUser() {
|
|
|
150
154
|
name: "template",
|
|
151
155
|
message: "Starting template:",
|
|
152
156
|
hint: " ",
|
|
157
|
+
initial: 1,
|
|
153
158
|
choices: [
|
|
154
159
|
{
|
|
155
160
|
title: "Minimal",
|
|
@@ -236,10 +241,10 @@ ${cyan("?")} ${bold(msg)}`);
|
|
|
236
241
|
function generatePackageJson(answers, projectDir) {
|
|
237
242
|
const deps = {
|
|
238
243
|
"@kyro-cms/core": "latest",
|
|
239
|
-
|
|
244
|
+
astro: "^5.4.0"
|
|
240
245
|
};
|
|
241
246
|
const devDeps = {
|
|
242
|
-
|
|
247
|
+
typescript: "^5.7.3"
|
|
243
248
|
};
|
|
244
249
|
if (answers.styling === "tailwind") {
|
|
245
250
|
deps["@astrojs/react"] = "^4.2.0";
|
|
@@ -260,13 +265,17 @@ function generatePackageJson(answers, projectDir) {
|
|
|
260
265
|
}
|
|
261
266
|
if (answers.admin) {
|
|
262
267
|
deps["@kyro-cms/admin"] = "latest";
|
|
268
|
+
deps["@astrojs/node"] = "^9.5.5";
|
|
263
269
|
deps["lucide-react"] = "^0.475.0";
|
|
264
270
|
}
|
|
265
271
|
const scripts = {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
272
|
+
dev: "astro dev",
|
|
273
|
+
build: "astro build",
|
|
274
|
+
preview: "astro preview"
|
|
269
275
|
};
|
|
276
|
+
if (answers.auth) {
|
|
277
|
+
scripts["db:bootstrap"] = "kyro auth bootstrap";
|
|
278
|
+
}
|
|
270
279
|
if (answers.database === "sqlite") {
|
|
271
280
|
scripts["db:generate"] = "kyro generate";
|
|
272
281
|
scripts["db:push"] = "kyro push";
|
|
@@ -288,26 +297,24 @@ function formatPackageJson(pkg) {
|
|
|
288
297
|
|
|
289
298
|
// src/generators/config.ts
|
|
290
299
|
function generateKyroConfig(answers) {
|
|
291
|
-
const imports = [
|
|
292
|
-
"import { defineConfig, createTemplateConfig } from '@kyro-cms/core';"
|
|
293
|
-
];
|
|
300
|
+
const imports = ["import { defineConfig } from '@kyro-cms/core';"];
|
|
294
301
|
const adapterLines = [];
|
|
295
302
|
if (answers.database === "sqlite") {
|
|
296
|
-
imports.push("import {
|
|
297
|
-
adapterLines.push(` adapter:
|
|
303
|
+
imports.push("import { createLocalAdapter } from '@kyro-cms/core';");
|
|
304
|
+
adapterLines.push(` adapter: createLocalAdapter({ path: './data.db' }),`);
|
|
298
305
|
} else if (answers.database === "postgres") {
|
|
299
|
-
imports.push("import {
|
|
300
|
-
adapterLines.push(` adapter:
|
|
306
|
+
imports.push("import { createDrizzleAdapter } from '@kyro-cms/core';");
|
|
307
|
+
adapterLines.push(` adapter: createDrizzleAdapter({`);
|
|
301
308
|
adapterLines.push(` connectionString: process.env.DATABASE_URL,`);
|
|
302
309
|
adapterLines.push(` }),`);
|
|
303
310
|
} else if (answers.database === "mysql") {
|
|
304
|
-
imports.push("import {
|
|
305
|
-
adapterLines.push(` adapter:
|
|
311
|
+
imports.push("import { createDrizzleAdapter } from '@kyro-cms/core';");
|
|
312
|
+
adapterLines.push(` adapter: createDrizzleAdapter({`);
|
|
306
313
|
adapterLines.push(` connectionString: process.env.DATABASE_URL,`);
|
|
307
314
|
adapterLines.push(` }),`);
|
|
308
315
|
} else if (answers.database === "mongodb") {
|
|
309
|
-
imports.push("import {
|
|
310
|
-
adapterLines.push(` adapter:
|
|
316
|
+
imports.push("import { createMongoDBAdapter } from '@kyro-cms/core';");
|
|
317
|
+
adapterLines.push(` adapter: createMongoDBAdapter({`);
|
|
311
318
|
adapterLines.push(` connectionString: process.env.MONGODB_URI,`);
|
|
312
319
|
adapterLines.push(` }),`);
|
|
313
320
|
}
|
|
@@ -331,39 +338,46 @@ function generateKyroConfig(answers) {
|
|
|
331
338
|
if (answers.versioning) {
|
|
332
339
|
features.push(" versioning: true,");
|
|
333
340
|
}
|
|
334
|
-
let templateCollections =
|
|
335
|
-
let templateGlobals =
|
|
341
|
+
let templateCollections = "";
|
|
342
|
+
let templateGlobals = "";
|
|
336
343
|
switch (answers.template) {
|
|
337
344
|
case "minimal":
|
|
338
|
-
templateCollections
|
|
345
|
+
templateCollections = "import { minimalCollections } from '@kyro-cms/core';";
|
|
339
346
|
break;
|
|
340
347
|
case "blog":
|
|
341
|
-
templateCollections
|
|
348
|
+
templateCollections = "import { blogCollections } from '@kyro-cms/core';";
|
|
342
349
|
break;
|
|
343
350
|
case "ecommerce":
|
|
344
|
-
templateCollections
|
|
351
|
+
templateCollections = "import { ecommerceCollections } from '@kyro-cms/core';";
|
|
345
352
|
break;
|
|
346
353
|
case "kitchen-sink":
|
|
347
|
-
templateCollections
|
|
348
|
-
"...minimalCollections,",
|
|
349
|
-
"...blogCollections,",
|
|
350
|
-
"...ecommerceCollections,",
|
|
351
|
-
"...kitchenSinkCollections,"
|
|
352
|
-
);
|
|
354
|
+
templateCollections = `import { minimalCollections, blogCollections, ecommerceCollections, kitchenSinkCollections } from '@kyro-cms/core';`;
|
|
353
355
|
break;
|
|
354
356
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
357
|
+
if (templateCollections) {
|
|
358
|
+
imports.push(templateCollections);
|
|
359
|
+
}
|
|
360
|
+
let collectionsConfig = "";
|
|
361
|
+
if (answers.template === "minimal") {
|
|
362
|
+
collectionsConfig = ` collections: Object.values(minimalCollections),`;
|
|
363
|
+
} else if (answers.template === "blog") {
|
|
364
|
+
collectionsConfig = ` collections: Object.values(blogCollections),`;
|
|
365
|
+
} else if (answers.template === "ecommerce") {
|
|
366
|
+
collectionsConfig = ` collections: Object.values(ecommerceCollections),`;
|
|
367
|
+
} else if (answers.template === "kitchen-sink") {
|
|
368
|
+
collectionsConfig = ` collections: [
|
|
369
|
+
...Object.values(minimalCollections),
|
|
370
|
+
...Object.values(blogCollections),
|
|
371
|
+
...Object.values(ecommerceCollections),
|
|
372
|
+
...Object.values(kitchenSinkCollections),
|
|
373
|
+
],`;
|
|
374
|
+
}
|
|
361
375
|
const config = `${imports.join("\n")}
|
|
362
376
|
|
|
363
377
|
export default defineConfig({
|
|
364
378
|
name: '${answers.projectName}',
|
|
365
379
|
prefix: '/api',${adapterLines.length > 0 ? "\n" + adapterLines.join("\n") : ""}
|
|
366
|
-
${
|
|
380
|
+
${collectionsConfig ? collectionsConfig : ""}${features.length > 0 ? "\n" + features.join("\n") : ""}
|
|
367
381
|
|
|
368
382
|
api: {
|
|
369
383
|
${apiConfig.join("\n")}
|
|
@@ -385,6 +399,7 @@ function generateAstroConfig(answers) {
|
|
|
385
399
|
mode: 'standalone'
|
|
386
400
|
}),` : "";
|
|
387
401
|
const config = `import { defineConfig } from 'astro/config';
|
|
402
|
+
import node from '@astrojs/node';
|
|
388
403
|
${answers.styling === "tailwind" ? "import react from '@astrojs/react';\nimport tailwindcss from '@tailwindcss/vite';" : ""}
|
|
389
404
|
|
|
390
405
|
export default defineConfig({
|
|
@@ -442,18 +457,67 @@ data/
|
|
|
442
457
|
|
|
443
458
|
A Kyro CMS project.
|
|
444
459
|
|
|
445
|
-
##
|
|
460
|
+
## Quick Start
|
|
446
461
|
|
|
447
462
|
\`\`\`bash
|
|
448
463
|
npm install
|
|
449
464
|
npm run dev
|
|
450
465
|
\`\`\`
|
|
451
466
|
|
|
467
|
+
## Admin Dashboard
|
|
468
|
+
|
|
469
|
+
Visit [http://localhost:4321/admin](http://localhost:4321/admin) to access the admin.
|
|
470
|
+
|
|
471
|
+
${answers.auth ? `## Creating Your Admin User
|
|
472
|
+
|
|
473
|
+
Before logging into the admin, you need to create an admin user. Run:
|
|
474
|
+
|
|
475
|
+
\`\`\`bash
|
|
476
|
+
npm run db:bootstrap
|
|
477
|
+
\`\`\`
|
|
478
|
+
|
|
479
|
+
Or set environment variables to auto-bootstrap on startup:
|
|
480
|
+
|
|
481
|
+
\`\`\`bash
|
|
482
|
+
# .env
|
|
483
|
+
KYRO_ADMIN_EMAIL=admin@example.com
|
|
484
|
+
KYRO_ADMIN_PASSWORD=SecurePass123!
|
|
485
|
+
\`\`\`
|
|
486
|
+
|
|
487
|
+
Then restart the dev server.
|
|
488
|
+
` : ""}
|
|
489
|
+
|
|
452
490
|
## Documentation
|
|
453
491
|
|
|
454
492
|
Visit [https://kyro.cms](https://kyro.cms) for full documentation.
|
|
455
493
|
`;
|
|
456
494
|
writeFileSync(join(projectDir, "README.md"), readme);
|
|
495
|
+
const envExample = `# Kyro CMS Configuration
|
|
496
|
+
|
|
497
|
+
${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\nDATABASE_SSL=false" : "# MongoDB connection\nMONGODB_URI=mongodb://localhost:27017/kyro_cms"}
|
|
498
|
+
|
|
499
|
+
${answers.auth ? `# Authentication (required for auth)
|
|
500
|
+
JWT_SECRET=change-this-to-a-random-32-character-string
|
|
501
|
+
JWT_EXPIRES_IN=24h
|
|
502
|
+
|
|
503
|
+
# Bootstrap admin user (creates on first run)
|
|
504
|
+
KYRO_ADMIN_EMAIL=admin@example.com
|
|
505
|
+
KYRO_ADMIN_PASSWORD=SecurePass123!
|
|
506
|
+
KYRO_ADMIN_ROLE=super_admin
|
|
507
|
+
|
|
508
|
+
# Optional: Redis for sessions (recommended for production)
|
|
509
|
+
# REDIS_URL=redis://localhost:6379
|
|
510
|
+
# REDIS_TLS=false
|
|
511
|
+
|
|
512
|
+
# Optional: SMTP for emails
|
|
513
|
+
# SMTP_HOST=smtp.example.com
|
|
514
|
+
# SMTP_PORT=587
|
|
515
|
+
# SMTP_SECURE=false
|
|
516
|
+
# SMTP_USER=your-email@example.com
|
|
517
|
+
# SMTP_PASS=your-password
|
|
518
|
+
# SMTP_FROM=noreply@example.com` : ""}
|
|
519
|
+
`;
|
|
520
|
+
writeFileSync(join(projectDir, ".env.example"), envExample);
|
|
457
521
|
const spec = `# ${answers.projectName}
|
|
458
522
|
|
|
459
523
|
## Overview
|
|
@@ -520,11 +584,11 @@ const title = "${answers.projectName}";
|
|
|
520
584
|
`;
|
|
521
585
|
writeFileSync(join(pagesDir, "index.astro"), indexPage);
|
|
522
586
|
if (answers.admin) {
|
|
523
|
-
const adminDir = join(
|
|
587
|
+
const adminDir = join(pagesDir, "admin");
|
|
524
588
|
mkdirSync(adminDir, { recursive: true });
|
|
525
589
|
const adminIndex = `---
|
|
526
590
|
import { Admin } from '@kyro-cms/admin';
|
|
527
|
-
import config from '
|
|
591
|
+
import config from '../../../kyro.config';
|
|
528
592
|
---
|
|
529
593
|
<!DOCTYPE html>
|
|
530
594
|
<html lang="en">
|
|
@@ -540,6 +604,448 @@ import config from '../kyro.config';
|
|
|
540
604
|
`;
|
|
541
605
|
writeFileSync(join(adminDir, "index.astro"), adminIndex);
|
|
542
606
|
}
|
|
607
|
+
if (answers.auth) {
|
|
608
|
+
const authApiDir = join(pagesDir, "api", "auth");
|
|
609
|
+
mkdirSync(authApiDir, { recursive: true });
|
|
610
|
+
writeFileSync(
|
|
611
|
+
join(authApiDir, "login.ts"),
|
|
612
|
+
generateLoginEndpoint(answers.database)
|
|
613
|
+
);
|
|
614
|
+
writeFileSync(
|
|
615
|
+
join(authApiDir, "register.ts"),
|
|
616
|
+
generateRegisterEndpoint(answers.database)
|
|
617
|
+
);
|
|
618
|
+
writeFileSync(
|
|
619
|
+
join(authApiDir, "logout.ts"),
|
|
620
|
+
generateLogoutEndpoint(answers.database)
|
|
621
|
+
);
|
|
622
|
+
writeFileSync(join(authApiDir, "me.ts"), generateMeEndpoint());
|
|
623
|
+
writeFileSync(
|
|
624
|
+
join(authApiDir, "users.ts"),
|
|
625
|
+
generateUsersEndpoint(answers.database)
|
|
626
|
+
);
|
|
627
|
+
writeFileSync(join(srcDir, "middleware.ts"), generateMiddleware());
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function generateLoginEndpoint(database) {
|
|
631
|
+
const adapterImport = database === "sqlite" ? `import { SQLiteAuthAdapter } from "@kyro-cms/core";` : `import { RedisAuthAdapter } from "@kyro-cms/core";`;
|
|
632
|
+
const adapterInit = database === "sqlite" ? ` return new SQLiteAuthAdapter({ path: "./data.db" });` : ` return new RedisAuthAdapter({
|
|
633
|
+
url: process.env.REDIS_URL || "redis://localhost:6379",
|
|
634
|
+
tls: process.env.REDIS_TLS === "true",
|
|
635
|
+
});`;
|
|
636
|
+
return `import type { APIRoute } from "astro";
|
|
637
|
+
${adapterImport}
|
|
638
|
+
import jwt from "jsonwebtoken";
|
|
639
|
+
|
|
640
|
+
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
641
|
+
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
|
|
642
|
+
|
|
643
|
+
async function getAuthApi() {
|
|
644
|
+
${adapterInit}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
648
|
+
try {
|
|
649
|
+
const body = (await request.json()) as {
|
|
650
|
+
email?: string;
|
|
651
|
+
password?: string;
|
|
652
|
+
};
|
|
653
|
+
const { email, password } = body;
|
|
654
|
+
|
|
655
|
+
if (!email || !password) {
|
|
656
|
+
return new Response(
|
|
657
|
+
JSON.stringify({ error: "Email and password required" }),
|
|
658
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const adapter = await getAuthApi();
|
|
663
|
+
await adapter.connect();
|
|
664
|
+
|
|
665
|
+
const user = await adapter.findUserByEmail(email);
|
|
666
|
+
if (!user || !user.passwordHash) {
|
|
667
|
+
await adapter.disconnect();
|
|
668
|
+
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
|
669
|
+
status: 401,
|
|
670
|
+
headers: { "Content-Type": "application/json" },
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const valid = await adapter.verifyPassword(password, user.passwordHash);
|
|
675
|
+
if (!valid) {
|
|
676
|
+
await adapter.disconnect();
|
|
677
|
+
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
|
678
|
+
status: 401,
|
|
679
|
+
headers: { "Content-Type": "application/json" },
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const session = await adapter.createSession(user.id, {
|
|
684
|
+
ipAddress: request.headers.get("x-forwarded-for") || "unknown",
|
|
685
|
+
userAgent: request.headers.get("user-agent") || "",
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const token = jwt.sign(
|
|
689
|
+
{
|
|
690
|
+
sub: user.id,
|
|
691
|
+
email: user.email,
|
|
692
|
+
role: user.role,
|
|
693
|
+
tenantId: user.tenantId,
|
|
694
|
+
},
|
|
695
|
+
JWT_SECRET,
|
|
696
|
+
{ expiresIn: JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
await adapter.disconnect();
|
|
700
|
+
|
|
701
|
+
const { passwordHash, ...safeUser } = user;
|
|
702
|
+
|
|
703
|
+
return new Response(
|
|
704
|
+
JSON.stringify({
|
|
705
|
+
success: true,
|
|
706
|
+
user: safeUser,
|
|
707
|
+
token,
|
|
708
|
+
refreshToken: session.refreshToken,
|
|
709
|
+
}),
|
|
710
|
+
{
|
|
711
|
+
status: 200,
|
|
712
|
+
headers: { "Content-Type": "application/json" },
|
|
713
|
+
},
|
|
714
|
+
);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
console.error("Login error:", error);
|
|
717
|
+
return new Response(JSON.stringify({ error: "Login failed" }), {
|
|
718
|
+
status: 500,
|
|
719
|
+
headers: { "Content-Type": "application/json" },
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
`;
|
|
724
|
+
}
|
|
725
|
+
function generateRegisterEndpoint(database) {
|
|
726
|
+
const adapterImport = database === "sqlite" ? `import { SQLiteAuthAdapter } from "@kyro-cms/core";` : `import { RedisAuthAdapter } from "@kyro-cms/core";`;
|
|
727
|
+
const adapterInit = database === "sqlite" ? ` return new SQLiteAuthAdapter({ path: "./data.db" });` : ` return new RedisAuthAdapter({
|
|
728
|
+
url: process.env.REDIS_URL || "redis://localhost:6379",
|
|
729
|
+
tls: process.env.REDIS_TLS === "true",
|
|
730
|
+
});`;
|
|
731
|
+
const isFirstUserCheck = database === "sqlite" ? ` const isFirstUser = !(await adapter.hasAnyUsers());` : ` const isFirstUser = await checkIsFirstUser(adapter);`;
|
|
732
|
+
const isFirstUserFn = database === "sqlite" ? "" : `
|
|
733
|
+
|
|
734
|
+
async function checkIsFirstUser(adapter: RedisAuthAdapter): Promise<boolean> {
|
|
735
|
+
try {
|
|
736
|
+
const redis = (adapter as any).redis;
|
|
737
|
+
if (!redis) return true;
|
|
738
|
+
const pattern = "kyro:auth:users:email:*";
|
|
739
|
+
const result = await redis.scan("0", "MATCH", pattern, "COUNT", "1");
|
|
740
|
+
const keys = result[1];
|
|
741
|
+
return keys.length === 0;
|
|
742
|
+
} catch {
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
745
|
+
}`;
|
|
746
|
+
return `import type { APIRoute } from "astro";
|
|
747
|
+
${adapterImport}
|
|
748
|
+
import jwt from "jsonwebtoken";
|
|
749
|
+
|
|
750
|
+
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
751
|
+
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
|
|
752
|
+
const ALLOW_REGISTRATION = process.env.KYRO_ALLOW_REGISTRATION !== "false";
|
|
753
|
+
|
|
754
|
+
async function getAuthApi() {
|
|
755
|
+
${adapterInit}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
759
|
+
try {
|
|
760
|
+
const body = (await request.json()) as {
|
|
761
|
+
email?: string;
|
|
762
|
+
password?: string;
|
|
763
|
+
confirmPassword?: string;
|
|
764
|
+
};
|
|
765
|
+
const { email, password, confirmPassword } = body;
|
|
766
|
+
|
|
767
|
+
if (!email || !password) {
|
|
768
|
+
return new Response(
|
|
769
|
+
JSON.stringify({ error: "Email and password required" }),
|
|
770
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (password !== confirmPassword) {
|
|
775
|
+
return new Response(
|
|
776
|
+
JSON.stringify({ error: "Passwords do not match" }),
|
|
777
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (password.length < 8) {
|
|
782
|
+
return new Response(
|
|
783
|
+
JSON.stringify({ error: "Password must be at least 8 characters" }),
|
|
784
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const adapter = await getAuthApi();
|
|
789
|
+
try {
|
|
790
|
+
await adapter.connect();
|
|
791
|
+
} catch {
|
|
792
|
+
return new Response(
|
|
793
|
+
JSON.stringify({ error: "Unable to connect to auth storage." }),
|
|
794
|
+
{ status: 500, headers: { "Content-Type": "application/json" } },
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const existingUser = await adapter.findUserByEmail(email);
|
|
799
|
+
if (existingUser) {
|
|
800
|
+
await adapter.disconnect();
|
|
801
|
+
return new Response(
|
|
802
|
+
JSON.stringify({ error: "Email already registered" }),
|
|
803
|
+
{ status: 409, headers: { "Content-Type": "application/json" } },
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
${isFirstUserCheck}
|
|
808
|
+
|
|
809
|
+
if (!isFirstUser && !ALLOW_REGISTRATION) {
|
|
810
|
+
await adapter.disconnect();
|
|
811
|
+
return new Response(
|
|
812
|
+
JSON.stringify({ error: "Registration is disabled" }),
|
|
813
|
+
{ status: 403, headers: { "Content-Type": "application/json" } },
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const passwordHash = await adapter.hashPassword(password);
|
|
818
|
+
const user = await adapter.createUser({
|
|
819
|
+
email,
|
|
820
|
+
passwordHash,
|
|
821
|
+
role: isFirstUser ? "super_admin" : "editor",
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
if (isFirstUser) {
|
|
825
|
+
await adapter.updateUser(user.id, { emailVerified: true });
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const session = await adapter.createSession(user.id, {
|
|
829
|
+
ipAddress: request.headers.get("x-forwarded-for") || "unknown",
|
|
830
|
+
userAgent: request.headers.get("user-agent") || "",
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
const token = jwt.sign(
|
|
834
|
+
{
|
|
835
|
+
sub: user.id,
|
|
836
|
+
email: user.email,
|
|
837
|
+
role: user.role,
|
|
838
|
+
tenantId: user.tenantId,
|
|
839
|
+
},
|
|
840
|
+
JWT_SECRET,
|
|
841
|
+
{ expiresIn: JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
await adapter.disconnect();
|
|
845
|
+
|
|
846
|
+
const { passwordHash: _, ...safeUser } = user;
|
|
847
|
+
|
|
848
|
+
return new Response(
|
|
849
|
+
JSON.stringify({
|
|
850
|
+
success: true,
|
|
851
|
+
isFirstUser,
|
|
852
|
+
user: safeUser,
|
|
853
|
+
token,
|
|
854
|
+
refreshToken: session.refreshToken,
|
|
855
|
+
}),
|
|
856
|
+
{
|
|
857
|
+
status: 201,
|
|
858
|
+
headers: { "Content-Type": "application/json" },
|
|
859
|
+
},
|
|
860
|
+
);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
console.error("Registration error:", error);
|
|
863
|
+
return new Response(JSON.stringify({ error: "Registration failed" }), {
|
|
864
|
+
status: 500,
|
|
865
|
+
headers: { "Content-Type": "application/json" },
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
};${isFirstUserFn}
|
|
869
|
+
`;
|
|
870
|
+
}
|
|
871
|
+
function generateLogoutEndpoint(database) {
|
|
872
|
+
const adapterImport = database === "sqlite" ? `import { SQLiteAuthAdapter } from "@kyro-cms/core";` : `import { RedisAuthAdapter } from "@kyro-cms/core";`;
|
|
873
|
+
const adapterInit = database === "sqlite" ? ` return new SQLiteAuthAdapter({ path: "./data.db" });` : ` return new RedisAuthAdapter({
|
|
874
|
+
url: process.env.REDIS_URL || "redis://localhost:6379",
|
|
875
|
+
tls: process.env.REDIS_TLS === "true",
|
|
876
|
+
});`;
|
|
877
|
+
return `import type { APIRoute } from "astro";
|
|
878
|
+
${adapterImport}
|
|
879
|
+
|
|
880
|
+
async function getAuthApi() {
|
|
881
|
+
${adapterInit}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
885
|
+
try {
|
|
886
|
+
const authHeader = request.headers.get("authorization");
|
|
887
|
+
const token = authHeader?.startsWith("Bearer ")
|
|
888
|
+
? authHeader.slice(7)
|
|
889
|
+
: null;
|
|
890
|
+
|
|
891
|
+
if (token) {
|
|
892
|
+
const adapter = await getAuthApi();
|
|
893
|
+
await adapter.connect();
|
|
894
|
+
await adapter.deleteSession(token);
|
|
895
|
+
await adapter.disconnect();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return new Response(
|
|
899
|
+
JSON.stringify({ success: true }),
|
|
900
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
901
|
+
);
|
|
902
|
+
} catch {
|
|
903
|
+
return new Response(
|
|
904
|
+
JSON.stringify({ success: true }),
|
|
905
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
`;
|
|
910
|
+
}
|
|
911
|
+
function generateMeEndpoint() {
|
|
912
|
+
return `import type { APIRoute } from "astro";
|
|
913
|
+
import jwt from "jsonwebtoken";
|
|
914
|
+
|
|
915
|
+
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
916
|
+
|
|
917
|
+
export const GET: APIRoute = async ({ request }) => {
|
|
918
|
+
const authHeader = request.headers.get("authorization");
|
|
919
|
+
const token = authHeader?.startsWith("Bearer ")
|
|
920
|
+
? authHeader.slice(7)
|
|
921
|
+
: null;
|
|
922
|
+
|
|
923
|
+
if (!token) {
|
|
924
|
+
return new Response(
|
|
925
|
+
JSON.stringify({ error: "Not authenticated" }),
|
|
926
|
+
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
try {
|
|
931
|
+
const payload = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
|
|
932
|
+
return new Response(
|
|
933
|
+
JSON.stringify({
|
|
934
|
+
id: payload.sub,
|
|
935
|
+
email: payload.email,
|
|
936
|
+
role: payload.role,
|
|
937
|
+
}),
|
|
938
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
939
|
+
);
|
|
940
|
+
} catch {
|
|
941
|
+
return new Response(
|
|
942
|
+
JSON.stringify({ error: "Invalid token" }),
|
|
943
|
+
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
`;
|
|
948
|
+
}
|
|
949
|
+
function generateUsersEndpoint(database) {
|
|
950
|
+
const adapterImport = database === "sqlite" ? `import { SQLiteAuthAdapter } from "@kyro-cms/core";` : `import { RedisAuthAdapter } from "@kyro-cms/core";`;
|
|
951
|
+
const adapterInit = database === "sqlite" ? ` return new SQLiteAuthAdapter({ path: "./data.db" });` : ` return new RedisAuthAdapter({
|
|
952
|
+
url: process.env.REDIS_URL || "redis://localhost:6379",
|
|
953
|
+
tls: process.env.REDIS_TLS === "true",
|
|
954
|
+
});`;
|
|
955
|
+
const hasUsersCheck = database === "sqlite" ? ` const hasUsers = await adapter.hasAnyUsers();` : ` const redis = (adapter as any).redis;
|
|
956
|
+
if (!redis) {
|
|
957
|
+
await adapter.disconnect();
|
|
958
|
+
return new Response(JSON.stringify({ hasUsers: false }), {
|
|
959
|
+
status: 200,
|
|
960
|
+
headers: { "Content-Type": "application/json" },
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const pattern = "kyro:auth:users:email:*";
|
|
965
|
+
const result = await redis.scan("0", "MATCH", pattern, "COUNT", "1");
|
|
966
|
+
const keys = result[1];
|
|
967
|
+
const hasUsers = keys.length > 0;`;
|
|
968
|
+
return `import type { APIRoute } from "astro";
|
|
969
|
+
${adapterImport}
|
|
970
|
+
|
|
971
|
+
async function getAuthApi() {
|
|
972
|
+
${adapterInit}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
export const GET: APIRoute = async () => {
|
|
976
|
+
try {
|
|
977
|
+
const adapter = await getAuthApi();
|
|
978
|
+
await adapter.connect();
|
|
979
|
+
|
|
980
|
+
${hasUsersCheck}
|
|
981
|
+
|
|
982
|
+
await adapter.disconnect();
|
|
983
|
+
|
|
984
|
+
return new Response(JSON.stringify({ hasUsers }), {
|
|
985
|
+
status: 200,
|
|
986
|
+
headers: { "Content-Type": "application/json" },
|
|
987
|
+
});
|
|
988
|
+
} catch {
|
|
989
|
+
return new Response(JSON.stringify({ hasUsers: false }), {
|
|
990
|
+
status: 200,
|
|
991
|
+
headers: { "Content-Type": "application/json" },
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
`;
|
|
996
|
+
}
|
|
997
|
+
function generateMiddleware() {
|
|
998
|
+
return `import type { MiddlewareHandler } from "astro";
|
|
999
|
+
import jwt from "jsonwebtoken";
|
|
1000
|
+
|
|
1001
|
+
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
1002
|
+
|
|
1003
|
+
const PUBLIC_PATHS = [
|
|
1004
|
+
"/api/auth/login",
|
|
1005
|
+
"/api/auth/logout",
|
|
1006
|
+
"/api/auth/register",
|
|
1007
|
+
"/api/auth/me",
|
|
1008
|
+
"/api/auth/users",
|
|
1009
|
+
"/api/health",
|
|
1010
|
+
"/favicon.svg",
|
|
1011
|
+
];
|
|
1012
|
+
|
|
1013
|
+
const PUBLIC_PREFIXES = ["/api/auth/", "/admin"];
|
|
1014
|
+
|
|
1015
|
+
export const onRequest: MiddlewareHandler = async ({ request, url }, next) => {
|
|
1016
|
+
const pathname = new URL(url).pathname;
|
|
1017
|
+
|
|
1018
|
+
if (PUBLIC_PATHS.includes(pathname) || PUBLIC_PATHS.includes(pathname.replace(/\\/$/, ""))) {
|
|
1019
|
+
return next();
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
for (const prefix of PUBLIC_PREFIXES) {
|
|
1023
|
+
if (pathname.startsWith(prefix)) {
|
|
1024
|
+
return next();
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const authHeader = request.headers.get("authorization");
|
|
1029
|
+
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
1030
|
+
|
|
1031
|
+
if (!token) {
|
|
1032
|
+
return new Response(
|
|
1033
|
+
JSON.stringify({ error: "Authentication required" }),
|
|
1034
|
+
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
try {
|
|
1039
|
+
jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
|
|
1040
|
+
return next();
|
|
1041
|
+
} catch {
|
|
1042
|
+
return new Response(
|
|
1043
|
+
JSON.stringify({ error: "Invalid or expired token" }),
|
|
1044
|
+
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
`;
|
|
543
1049
|
}
|
|
544
1050
|
|
|
545
1051
|
// src/index.ts
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-kyro",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Interactive scaffolding for Kyro CMS projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"create-kyro": "
|
|
7
|
+
"create-kyro": "bin.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "./dist/index.js",
|
|
10
10
|
"types": "./dist/index.d.ts",
|
package/my-kyro-app/README.md
DELETED
package/my-kyro-app/SPEC.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# my-kyro-app
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
This project uses Kyro CMS - an Astro-native headless CMS.
|
|
6
|
-
|
|
7
|
-
## Configuration
|
|
8
|
-
|
|
9
|
-
- **Database**: SQLite (local-first)
|
|
10
|
-
- **APIs**: rest, graphql
|
|
11
|
-
- **Styling**: tailwind
|
|
12
|
-
- **Auth**: Disabled
|
|
13
|
-
- **Versioning**: Enabled
|
|
14
|
-
- **Admin**: Included
|
|
15
|
-
- **Template**: blog
|
|
16
|
-
|
|
17
|
-
## Collections
|
|
18
|
-
|
|
19
|
-
- Posts
|
|
20
|
-
- Categories
|
|
21
|
-
- Media
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'astro/config';
|
|
2
|
-
import react from '@astrojs/react';
|
|
3
|
-
import tailwindcss from '@tailwindcss/vite';
|
|
4
|
-
|
|
5
|
-
export default defineConfig({
|
|
6
|
-
output: 'server',
|
|
7
|
-
adapter: node({
|
|
8
|
-
mode: 'standalone'
|
|
9
|
-
}),
|
|
10
|
-
|
|
11
|
-
integrations: [
|
|
12
|
-
react(),
|
|
13
|
-
],
|
|
14
|
-
vite: {
|
|
15
|
-
plugins: [
|
|
16
|
-
tailwindcss(),
|
|
17
|
-
],
|
|
18
|
-
},
|
|
19
|
-
server: {
|
|
20
|
-
port: 4321,
|
|
21
|
-
host: true,
|
|
22
|
-
},
|
|
23
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from '@kyro-cms/core';
|
|
2
|
-
import { localAdapter } from '@kyro-cms/core';
|
|
3
|
-
import { blogCollections } from '@kyro-cms/core';
|
|
4
|
-
|
|
5
|
-
export default defineConfig({
|
|
6
|
-
name: 'my-kyro-app',
|
|
7
|
-
prefix: '/api',
|
|
8
|
-
adapter: localAdapter({ path: './data.db' }),
|
|
9
|
-
|
|
10
|
-
collections: {
|
|
11
|
-
...blogCollections,
|
|
12
|
-
},
|
|
13
|
-
versioning: true,
|
|
14
|
-
|
|
15
|
-
api: {
|
|
16
|
-
rest: true,
|
|
17
|
-
graphql: true,
|
|
18
|
-
},
|
|
19
|
-
});
|
package/my-kyro-app/package.json
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "my-kyro-app",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"type": "module",
|
|
5
|
-
"private": true,
|
|
6
|
-
"scripts": {
|
|
7
|
-
"dev": "astro dev",
|
|
8
|
-
"build": "astro build",
|
|
9
|
-
"preview": "astro preview",
|
|
10
|
-
"db:generate": "kyro generate",
|
|
11
|
-
"db:push": "kyro push",
|
|
12
|
-
"db:studio": "kyro studio"
|
|
13
|
-
},
|
|
14
|
-
"dependencies": {
|
|
15
|
-
"@kyro-cms/core": "latest",
|
|
16
|
-
"astro": "^5.4.0",
|
|
17
|
-
"@astrojs/react": "^4.2.0",
|
|
18
|
-
"react": "^19.0.0",
|
|
19
|
-
"react-dom": "^19.0.0",
|
|
20
|
-
"tailwindcss": "^4.0.0",
|
|
21
|
-
"@tailwindcss/vite": "^4.0.0",
|
|
22
|
-
"@kyro-cms/admin": "latest",
|
|
23
|
-
"lucide-react": "^0.475.0"
|
|
24
|
-
},
|
|
25
|
-
"devDependencies": {
|
|
26
|
-
"typescript": "^5.7.3",
|
|
27
|
-
"@types/react": "^19.0.0",
|
|
28
|
-
"@types/react-dom": "^19.0.0"
|
|
29
|
-
}
|
|
30
|
-
}
|