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