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 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
- "astro": "^5.4.0"
242
+ astro: "^5.4.0"
240
243
  };
241
244
  const devDeps = {
242
- "typescript": "^5.7.3"
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
- "dev": "astro dev",
267
- "build": "astro build",
268
- "preview": "astro preview"
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 { localAdapter } from '@kyro-cms/core';");
297
- adapterLines.push(` adapter: localAdapter({ path: './data.db' }),`);
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 { drizzleAdapter } from '@kyro-cms/core';");
300
- adapterLines.push(` adapter: drizzleAdapter({`);
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 { drizzleAdapter } from '@kyro-cms/core';");
305
- adapterLines.push(` adapter: drizzleAdapter({`);
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 { mongoAdapter } from '@kyro-cms/core';");
310
- adapterLines.push(` adapter: mongoAdapter({`);
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.push("...minimalCollections,");
343
+ templateCollections = "import { minimalCollections } from '@kyro-cms/core';";
339
344
  break;
340
345
  case "blog":
341
- templateCollections.push("...blogCollections,");
346
+ templateCollections = "import { blogCollections } from '@kyro-cms/core';";
342
347
  break;
343
348
  case "ecommerce":
344
- templateCollections.push("...ecommerceCollections,");
349
+ templateCollections = "import { ecommerceCollections } from '@kyro-cms/core';";
345
350
  break;
346
351
  case "kitchen-sink":
347
- templateCollections.push(
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
- const collectionsBlock = templateCollections.length > 0 ? ` collections: [
356
- ${templateCollections.join("\n ")}
357
- ],` : "";
358
- const globalsBlock = templateGlobals.length > 0 ? ` globals: [
359
- ${templateGlobals.join("\n ")}
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
- ${collectionsBlock ? "\n" + collectionsBlock : ""}${globalsBlock ? "\n" + globalsBlock : ""}${features.length > 0 ? "\n" + features.join("\n") : ""}
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
- ## Getting Started
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(srcDir, "admin");
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 '../kyro.config';
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