create-kyro 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -13
- package/dist/index.js +67 -664
- package/package.json +1 -1
- package/src/generators/astro.ts +7 -70
- package/src/generators/config.ts +39 -40
- package/src/generators/files.ts +18 -509
- package/src/generators/packagejson.ts +6 -35
- package/src/index.ts +5 -5
- package/src/prompts.ts +1 -62
- package/test/generators.test.ts +13 -100
- package/test/validators.test.ts +0 -1
package/src/generators/files.ts
CHANGED
|
@@ -14,7 +14,11 @@ export function generateProjectFiles(
|
|
|
14
14
|
mkdirSync(publicDir, { recursive: true });
|
|
15
15
|
|
|
16
16
|
if (answers.database === "sqlite") {
|
|
17
|
-
|
|
17
|
+
envDbSection = "# SQLite (local) - no additional config needed";
|
|
18
|
+
} else if (answers.database === "postgres") {
|
|
19
|
+
envDbSection = "# Database connection (PostgreSQL)\nDATABASE_URL=postgresql://user:password@localhost:5432/kyro_cms";
|
|
20
|
+
} else {
|
|
21
|
+
envDbSection = "# MongoDB connection\nMONGODB_URI=mongodb://localhost:27017/kyro_cms";
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
const tsconfig = `{
|
|
@@ -56,100 +60,36 @@ npm run dev
|
|
|
56
60
|
|
|
57
61
|
Visit [http://localhost:4321/admin](http://localhost:4321/admin) to access the admin.
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
answers.auth
|
|
61
|
-
? `## Creating Your Admin User
|
|
62
|
-
|
|
63
|
-
Before logging into the admin, you need to create an admin user. Run:
|
|
64
|
-
|
|
65
|
-
\`\`\`bash
|
|
66
|
-
npm run db:bootstrap
|
|
67
|
-
\`\`\`
|
|
68
|
-
|
|
69
|
-
Or set environment variables to auto-bootstrap on startup:
|
|
70
|
-
|
|
71
|
-
\`\`\`bash
|
|
72
|
-
# .env
|
|
73
|
-
KYRO_ADMIN_EMAIL=admin@example.com
|
|
74
|
-
KYRO_ADMIN_PASSWORD=SecurePass123!
|
|
75
|
-
\`\`\`
|
|
76
|
-
|
|
77
|
-
Then restart the dev server.
|
|
78
|
-
`
|
|
79
|
-
: ""
|
|
80
|
-
}
|
|
63
|
+
The first user to register will automatically be granted super admin privileges.
|
|
81
64
|
|
|
82
65
|
## Documentation
|
|
83
66
|
|
|
84
|
-
Visit [https://kyro.
|
|
67
|
+
Visit [https://kyro.dev](https://kyro.dev) for full documentation.
|
|
85
68
|
`;
|
|
86
69
|
|
|
87
70
|
writeFileSync(join(projectDir, "README.md"), readme);
|
|
88
71
|
|
|
89
72
|
const envExample = `# Kyro CMS Configuration
|
|
73
|
+
# Copy this file to .env and fill in your values
|
|
90
74
|
|
|
91
75
|
${
|
|
92
76
|
answers.database === "sqlite"
|
|
93
77
|
? "# SQLite (local) - no additional config needed"
|
|
94
|
-
: answers.database === "postgres"
|
|
95
|
-
? "# Database connection (PostgreSQL
|
|
78
|
+
: answers.database === "postgres"
|
|
79
|
+
? "# Database connection (PostgreSQL)\nDATABASE_URL=postgresql://user:password@localhost:5432/kyro_cms"
|
|
96
80
|
: "# MongoDB connection\nMONGODB_URI=mongodb://localhost:27017/kyro_cms"
|
|
97
81
|
}
|
|
98
82
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
? `# Authentication (uses SQLite at ./data/auth.db - no Redis needed)
|
|
102
|
-
JWT_SECRET=change-this-to-a-random-32-character-string
|
|
103
|
-
JWT_EXPIRES_IN=24h
|
|
104
|
-
|
|
105
|
-
# Registration control (set to false to disable public registration after first user)
|
|
106
|
-
KYRO_ALLOW_REGISTRATION=true
|
|
107
|
-
|
|
108
|
-
# Optional: Custom auth database path (default: ./data/auth.db)
|
|
109
|
-
# KYRO_AUTH_DB_PATH=./data/auth.db
|
|
83
|
+
# JWT secret for authentication tokens
|
|
84
|
+
JWT_SECRET=change-this-to-a-random-64-character-string
|
|
110
85
|
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
# SMTP_SECURE=false
|
|
115
|
-
# SMTP_USER=your-email@example.com
|
|
116
|
-
# SMTP_PASS=your-password
|
|
117
|
-
# SMTP_FROM=noreply@example.com`
|
|
118
|
-
: ""
|
|
119
|
-
}
|
|
86
|
+
# Admin credentials (used for first-user bootstrap)
|
|
87
|
+
# KYRO_ADMIN_EMAIL=admin@example.com
|
|
88
|
+
# KYRO_ADMIN_PASSWORD=SecurePass123!
|
|
120
89
|
`;
|
|
121
90
|
|
|
122
91
|
writeFileSync(join(projectDir, ".env.example"), envExample);
|
|
123
92
|
|
|
124
|
-
const spec = `# ${answers.projectName}
|
|
125
|
-
|
|
126
|
-
## Overview
|
|
127
|
-
|
|
128
|
-
This project uses Kyro CMS - an Astro-native headless CMS.
|
|
129
|
-
|
|
130
|
-
## Configuration
|
|
131
|
-
|
|
132
|
-
- **Database**: ${answers.database === "sqlite" ? "SQLite (local-first)" : answers.database}
|
|
133
|
-
- **APIs**: REST, GraphQL, tRPC, WebSocket
|
|
134
|
-
- **Styling**: ${answers.styling}
|
|
135
|
-
- **Auth**: ${answers.auth ? "Enabled" : "Disabled"}
|
|
136
|
-
- **Versioning**: ${answers.versioning ? "Enabled" : "Disabled"}
|
|
137
|
-
- **Admin**: ${answers.admin ? "Included" : "Not included"}
|
|
138
|
-
- **Template**: ${answers.template}
|
|
139
|
-
|
|
140
|
-
## Collections
|
|
141
|
-
|
|
142
|
-
${
|
|
143
|
-
answers.template === "minimal"
|
|
144
|
-
? "- Posts"
|
|
145
|
-
: answers.template === "blog"
|
|
146
|
-
? "- Posts\n- Categories\n- Media"
|
|
147
|
-
: "- Products\n- Categories\n- Customers\n- Orders\n- Coupons"
|
|
148
|
-
}
|
|
149
|
-
`;
|
|
150
|
-
|
|
151
|
-
writeFileSync(join(projectDir, "SPEC.md"), spec);
|
|
152
|
-
|
|
153
93
|
const indexPage = `---
|
|
154
94
|
const title = "${answers.projectName}";
|
|
155
95
|
---
|
|
@@ -158,17 +98,13 @@ const title = "${answers.projectName}";
|
|
|
158
98
|
<head>
|
|
159
99
|
<meta charset="UTF-8" />
|
|
160
100
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
161
|
-
<title
|
|
101
|
+
<title>{title}</title>
|
|
162
102
|
</head>
|
|
163
103
|
<body>
|
|
164
104
|
<main>
|
|
165
|
-
<h1>Welcome to
|
|
105
|
+
<h1>Welcome to {title}</h1>
|
|
166
106
|
<p>Your Kyro CMS is ready.</p>
|
|
167
|
-
|
|
168
|
-
answers.admin
|
|
169
|
-
? '<p><a href="/admin">Go to Admin Dashboard →</a></p>'
|
|
170
|
-
: ""
|
|
171
|
-
}
|
|
107
|
+
<p><a href="/admin">Go to Admin Dashboard →</a></p>
|
|
172
108
|
</main>
|
|
173
109
|
</body>
|
|
174
110
|
</html>
|
|
@@ -199,431 +135,4 @@ const title = "${answers.projectName}";
|
|
|
199
135
|
`;
|
|
200
136
|
|
|
201
137
|
writeFileSync(join(pagesDir, "index.astro"), indexPage);
|
|
202
|
-
|
|
203
|
-
if (answers.admin) {
|
|
204
|
-
const adminDir = join(pagesDir, "admin");
|
|
205
|
-
mkdirSync(adminDir, { recursive: true });
|
|
206
|
-
|
|
207
|
-
const adminIndex = `---
|
|
208
|
-
import { Admin } from '@kyro-cms/admin';
|
|
209
|
-
import config from '../../../kyro.config';
|
|
210
|
-
---
|
|
211
|
-
<!DOCTYPE html>
|
|
212
|
-
<html lang="en">
|
|
213
|
-
<head>
|
|
214
|
-
<meta charset="UTF-8" />
|
|
215
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
216
|
-
<title>Admin - ${answers.projectName}</title>
|
|
217
|
-
</head>
|
|
218
|
-
<body>
|
|
219
|
-
<Admin client:load config={config} />
|
|
220
|
-
</body>
|
|
221
|
-
</html>
|
|
222
|
-
`;
|
|
223
|
-
|
|
224
|
-
writeFileSync(join(adminDir, "index.astro"), adminIndex);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (answers.auth) {
|
|
228
|
-
const authApiDir = join(pagesDir, "api", "auth");
|
|
229
|
-
mkdirSync(authApiDir, { recursive: true });
|
|
230
|
-
|
|
231
|
-
writeFileSync(
|
|
232
|
-
join(authApiDir, "login.ts"),
|
|
233
|
-
generateLoginEndpoint(answers.database),
|
|
234
|
-
);
|
|
235
|
-
writeFileSync(
|
|
236
|
-
join(authApiDir, "register.ts"),
|
|
237
|
-
generateRegisterEndpoint(answers.database),
|
|
238
|
-
);
|
|
239
|
-
writeFileSync(
|
|
240
|
-
join(authApiDir, "logout.ts"),
|
|
241
|
-
generateLogoutEndpoint(answers.database),
|
|
242
|
-
);
|
|
243
|
-
writeFileSync(join(authApiDir, "me.ts"), generateMeEndpoint());
|
|
244
|
-
writeFileSync(
|
|
245
|
-
join(authApiDir, "users.ts"),
|
|
246
|
-
generateUsersEndpoint(answers.database),
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
writeFileSync(join(srcDir, "middleware.ts"), generateMiddleware());
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function generateLoginEndpoint(database: string): string {
|
|
254
|
-
return `import type { APIRoute } from "astro";
|
|
255
|
-
import { SQLiteAuthAdapter } from "@kyro-cms/core";
|
|
256
|
-
import jwt from "jsonwebtoken";
|
|
257
|
-
|
|
258
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
259
|
-
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
|
|
260
|
-
|
|
261
|
-
async function getAuthApi() {
|
|
262
|
-
return new SQLiteAuthAdapter({ path: "./data/auth.db" });
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export const POST: APIRoute = async ({ request }) => {
|
|
266
|
-
try {
|
|
267
|
-
const body = (await request.json()) as {
|
|
268
|
-
email?: string;
|
|
269
|
-
password?: string;
|
|
270
|
-
};
|
|
271
|
-
const { email, password } = body;
|
|
272
|
-
|
|
273
|
-
if (!email || !password) {
|
|
274
|
-
return new Response(
|
|
275
|
-
JSON.stringify({ error: "Email and password required" }),
|
|
276
|
-
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const adapter = await getAuthApi();
|
|
281
|
-
await adapter.connect();
|
|
282
|
-
|
|
283
|
-
const user = await adapter.findUserByEmail(email);
|
|
284
|
-
if (!user || !user.passwordHash) {
|
|
285
|
-
await adapter.disconnect();
|
|
286
|
-
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
|
287
|
-
status: 401,
|
|
288
|
-
headers: { "Content-Type": "application/json" },
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const valid = await adapter.verifyPassword(password, user.passwordHash);
|
|
293
|
-
if (!valid) {
|
|
294
|
-
await adapter.disconnect();
|
|
295
|
-
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
|
296
|
-
status: 401,
|
|
297
|
-
headers: { "Content-Type": "application/json" },
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const session = await adapter.createSession(user.id, {
|
|
302
|
-
ipAddress: request.headers.get("x-forwarded-for") || "unknown",
|
|
303
|
-
userAgent: request.headers.get("user-agent") || "",
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
const token = jwt.sign(
|
|
307
|
-
{
|
|
308
|
-
sub: user.id,
|
|
309
|
-
email: user.email,
|
|
310
|
-
role: user.role,
|
|
311
|
-
tenantId: user.tenantId,
|
|
312
|
-
},
|
|
313
|
-
JWT_SECRET,
|
|
314
|
-
{ expiresIn: JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
|
|
315
|
-
);
|
|
316
|
-
|
|
317
|
-
await adapter.disconnect();
|
|
318
|
-
|
|
319
|
-
const { passwordHash, ...safeUser } = user;
|
|
320
|
-
|
|
321
|
-
return new Response(
|
|
322
|
-
JSON.stringify({
|
|
323
|
-
success: true,
|
|
324
|
-
user: safeUser,
|
|
325
|
-
token,
|
|
326
|
-
refreshToken: session.refreshToken,
|
|
327
|
-
}),
|
|
328
|
-
{
|
|
329
|
-
status: 200,
|
|
330
|
-
headers: { "Content-Type": "application/json" },
|
|
331
|
-
},
|
|
332
|
-
);
|
|
333
|
-
} catch (error) {
|
|
334
|
-
console.error("Login error:", error);
|
|
335
|
-
return new Response(JSON.stringify({ error: "Login failed" }), {
|
|
336
|
-
status: 500,
|
|
337
|
-
headers: { "Content-Type": "application/json" },
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
`;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function generateRegisterEndpoint(database: string): string {
|
|
345
|
-
return `import type { APIRoute } from "astro";
|
|
346
|
-
import { SQLiteAuthAdapter } from "@kyro-cms/core";
|
|
347
|
-
import jwt from "jsonwebtoken";
|
|
348
|
-
|
|
349
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
350
|
-
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "24h";
|
|
351
|
-
const ALLOW_REGISTRATION = process.env.KYRO_ALLOW_REGISTRATION !== "false";
|
|
352
|
-
|
|
353
|
-
async function getAuthApi() {
|
|
354
|
-
return new SQLiteAuthAdapter({ path: "./data/auth.db" });
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
export const POST: APIRoute = async ({ request }) => {
|
|
358
|
-
try {
|
|
359
|
-
const body = (await request.json()) as {
|
|
360
|
-
email?: string;
|
|
361
|
-
password?: string;
|
|
362
|
-
confirmPassword?: string;
|
|
363
|
-
};
|
|
364
|
-
const { email, password, confirmPassword } = body;
|
|
365
|
-
|
|
366
|
-
if (!email || !password) {
|
|
367
|
-
return new Response(
|
|
368
|
-
JSON.stringify({ error: "Email and password required" }),
|
|
369
|
-
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (password !== confirmPassword) {
|
|
374
|
-
return new Response(
|
|
375
|
-
JSON.stringify({ error: "Passwords do not match" }),
|
|
376
|
-
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (password.length < 8) {
|
|
381
|
-
return new Response(
|
|
382
|
-
JSON.stringify({ error: "Password must be at least 8 characters" }),
|
|
383
|
-
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const adapter = await getAuthApi();
|
|
388
|
-
try {
|
|
389
|
-
await adapter.connect();
|
|
390
|
-
} catch {
|
|
391
|
-
return new Response(
|
|
392
|
-
JSON.stringify({ error: "Unable to connect to auth storage." }),
|
|
393
|
-
{ status: 500, headers: { "Content-Type": "application/json" } },
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const existingUser = await adapter.findUserByEmail(email);
|
|
398
|
-
if (existingUser) {
|
|
399
|
-
await adapter.disconnect();
|
|
400
|
-
return new Response(
|
|
401
|
-
JSON.stringify({ error: "Email already registered" }),
|
|
402
|
-
{ status: 409, headers: { "Content-Type": "application/json" } },
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const isFirstUser = !(await adapter.hasAnyUsers());
|
|
407
|
-
|
|
408
|
-
if (!isFirstUser && !ALLOW_REGISTRATION) {
|
|
409
|
-
await adapter.disconnect();
|
|
410
|
-
return new Response(
|
|
411
|
-
JSON.stringify({ error: "Registration is disabled" }),
|
|
412
|
-
{ status: 403, headers: { "Content-Type": "application/json" } },
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const passwordHash = await adapter.hashPassword(password);
|
|
417
|
-
const user = await adapter.createUser({
|
|
418
|
-
email,
|
|
419
|
-
passwordHash,
|
|
420
|
-
role: isFirstUser ? "super_admin" : "editor",
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
if (isFirstUser) {
|
|
424
|
-
await adapter.updateUser(user.id, { emailVerified: true });
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const session = await adapter.createSession(user.id, {
|
|
428
|
-
ipAddress: request.headers.get("x-forwarded-for") || "unknown",
|
|
429
|
-
userAgent: request.headers.get("user-agent") || "",
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
const token = jwt.sign(
|
|
433
|
-
{
|
|
434
|
-
sub: user.id,
|
|
435
|
-
email: user.email,
|
|
436
|
-
role: user.role,
|
|
437
|
-
tenantId: user.tenantId,
|
|
438
|
-
},
|
|
439
|
-
JWT_SECRET,
|
|
440
|
-
{ expiresIn: JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
|
|
441
|
-
);
|
|
442
|
-
|
|
443
|
-
await adapter.disconnect();
|
|
444
|
-
|
|
445
|
-
const { passwordHash: _, ...safeUser } = user;
|
|
446
|
-
|
|
447
|
-
return new Response(
|
|
448
|
-
JSON.stringify({
|
|
449
|
-
success: true,
|
|
450
|
-
isFirstUser,
|
|
451
|
-
user: safeUser,
|
|
452
|
-
token,
|
|
453
|
-
refreshToken: session.refreshToken,
|
|
454
|
-
}),
|
|
455
|
-
{
|
|
456
|
-
status: 201,
|
|
457
|
-
headers: { "Content-Type": "application/json" },
|
|
458
|
-
},
|
|
459
|
-
);
|
|
460
|
-
} catch (error) {
|
|
461
|
-
console.error("Registration error:", error);
|
|
462
|
-
return new Response(JSON.stringify({ error: "Registration failed" }), {
|
|
463
|
-
status: 500,
|
|
464
|
-
headers: { "Content-Type": "application/json" },
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
`;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function generateLogoutEndpoint(database: string): string {
|
|
472
|
-
return `import type { APIRoute } from "astro";
|
|
473
|
-
import { SQLiteAuthAdapter } from "@kyro-cms/core";
|
|
474
|
-
|
|
475
|
-
async function getAuthApi() {
|
|
476
|
-
return new SQLiteAuthAdapter({ path: "./data/auth.db" });
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
export const POST: APIRoute = async ({ request }) => {
|
|
480
|
-
try {
|
|
481
|
-
const authHeader = request.headers.get("authorization");
|
|
482
|
-
const token = authHeader?.startsWith("Bearer ")
|
|
483
|
-
? authHeader.slice(7)
|
|
484
|
-
: null;
|
|
485
|
-
|
|
486
|
-
if (token) {
|
|
487
|
-
const adapter = await getAuthApi();
|
|
488
|
-
await adapter.connect();
|
|
489
|
-
await adapter.deleteSession(token);
|
|
490
|
-
await adapter.disconnect();
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
return new Response(
|
|
494
|
-
JSON.stringify({ success: true }),
|
|
495
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
496
|
-
);
|
|
497
|
-
} catch {
|
|
498
|
-
return new Response(
|
|
499
|
-
JSON.stringify({ success: true }),
|
|
500
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
};
|
|
504
|
-
`;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function generateMeEndpoint(): string {
|
|
508
|
-
return `import type { APIRoute } from "astro";
|
|
509
|
-
import jwt from "jsonwebtoken";
|
|
510
|
-
|
|
511
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
512
|
-
|
|
513
|
-
export const GET: APIRoute = async ({ request }) => {
|
|
514
|
-
const authHeader = request.headers.get("authorization");
|
|
515
|
-
const token = authHeader?.startsWith("Bearer ")
|
|
516
|
-
? authHeader.slice(7)
|
|
517
|
-
: null;
|
|
518
|
-
|
|
519
|
-
if (!token) {
|
|
520
|
-
return new Response(
|
|
521
|
-
JSON.stringify({ error: "Not authenticated" }),
|
|
522
|
-
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
try {
|
|
527
|
-
const payload = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
|
|
528
|
-
return new Response(
|
|
529
|
-
JSON.stringify({
|
|
530
|
-
id: payload.sub,
|
|
531
|
-
email: payload.email,
|
|
532
|
-
role: payload.role,
|
|
533
|
-
}),
|
|
534
|
-
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
535
|
-
);
|
|
536
|
-
} catch {
|
|
537
|
-
return new Response(
|
|
538
|
-
JSON.stringify({ error: "Invalid token" }),
|
|
539
|
-
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
540
|
-
);
|
|
541
|
-
}
|
|
542
|
-
};
|
|
543
|
-
`;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function generateUsersEndpoint(database: string): string {
|
|
547
|
-
return `import type { APIRoute } from "astro";
|
|
548
|
-
import { SQLiteAuthAdapter } from "@kyro-cms/core";
|
|
549
|
-
|
|
550
|
-
async function getAuthApi() {
|
|
551
|
-
return new SQLiteAuthAdapter({ path: "./data/auth.db" });
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
export const GET: APIRoute = async () => {
|
|
555
|
-
try {
|
|
556
|
-
const adapter = await getAuthApi();
|
|
557
|
-
await adapter.connect();
|
|
558
|
-
|
|
559
|
-
const hasUsers = await adapter.hasAnyUsers();
|
|
560
|
-
|
|
561
|
-
await adapter.disconnect();
|
|
562
|
-
|
|
563
|
-
return new Response(JSON.stringify({ hasUsers }), {
|
|
564
|
-
status: 200,
|
|
565
|
-
headers: { "Content-Type": "application/json" },
|
|
566
|
-
});
|
|
567
|
-
} catch {
|
|
568
|
-
return new Response(JSON.stringify({ hasUsers: false }), {
|
|
569
|
-
status: 200,
|
|
570
|
-
headers: { "Content-Type": "application/json" },
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
};
|
|
574
|
-
`;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function generateMiddleware(): string {
|
|
578
|
-
return `import type { MiddlewareHandler } from "astro";
|
|
579
|
-
import jwt from "jsonwebtoken";
|
|
580
|
-
|
|
581
|
-
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
|
582
|
-
|
|
583
|
-
const PUBLIC_PATHS = [
|
|
584
|
-
"/api/auth/login",
|
|
585
|
-
"/api/auth/logout",
|
|
586
|
-
"/api/auth/register",
|
|
587
|
-
"/api/auth/me",
|
|
588
|
-
"/api/auth/users",
|
|
589
|
-
"/api/health",
|
|
590
|
-
"/favicon.svg",
|
|
591
|
-
];
|
|
592
|
-
|
|
593
|
-
const PUBLIC_PREFIXES = ["/api/auth/", "/admin"];
|
|
594
|
-
|
|
595
|
-
export const onRequest: MiddlewareHandler = async ({ request, url }, next) => {
|
|
596
|
-
const pathname = new URL(url).pathname;
|
|
597
|
-
|
|
598
|
-
if (PUBLIC_PATHS.includes(pathname) || PUBLIC_PATHS.includes(pathname.replace(/\\/$/, ""))) {
|
|
599
|
-
return next();
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
for (const prefix of PUBLIC_PREFIXES) {
|
|
603
|
-
if (pathname.startsWith(prefix)) {
|
|
604
|
-
return next();
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const authHeader = request.headers.get("authorization");
|
|
609
|
-
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
610
|
-
|
|
611
|
-
if (!token) {
|
|
612
|
-
return new Response(
|
|
613
|
-
JSON.stringify({ error: "Authentication required" }),
|
|
614
|
-
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
try {
|
|
619
|
-
jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
|
|
620
|
-
return next();
|
|
621
|
-
} catch {
|
|
622
|
-
return new Response(
|
|
623
|
-
JSON.stringify({ error: "Invalid or expired token" }),
|
|
624
|
-
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
};
|
|
628
|
-
`;
|
|
629
138
|
}
|
|
@@ -12,52 +12,23 @@ export interface PackageJson {
|
|
|
12
12
|
|
|
13
13
|
export function generatePackageJson(
|
|
14
14
|
answers: Answers,
|
|
15
|
-
projectDir: string,
|
|
16
15
|
): PackageJson {
|
|
17
16
|
const deps: Record<string, string> = {
|
|
17
|
+
"astro": "^6.3.1",
|
|
18
18
|
"@kyro-cms/core": "latest",
|
|
19
|
-
|
|
19
|
+
"@kyro-cms/admin": "latest",
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
const devDeps: Record<string, string> = {
|
|
23
|
-
typescript: "^5.7.3",
|
|
23
|
+
"typescript": "^5.7.3",
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
if (answers.styling === "tailwind") {
|
|
27
|
-
deps["@astrojs/react"] = "^4.2.0";
|
|
28
|
-
deps["react"] = "^19.0.0";
|
|
29
|
-
deps["react-dom"] = "^19.0.0";
|
|
30
|
-
deps["tailwindcss"] = "^4.0.0";
|
|
31
|
-
deps["@tailwindcss/vite"] = "^4.0.0";
|
|
32
|
-
devDeps["@types/react"] = "^19.0.0";
|
|
33
|
-
devDeps["@types/react-dom"] = "^19.0.0";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (answers.database === "postgres") {
|
|
37
|
-
deps["pg"] = "^8.13.1";
|
|
38
|
-
deps["@types/pg"] = "^8.11.0";
|
|
39
|
-
} else if (answers.database === "mysql") {
|
|
40
|
-
deps["mysql2"] = "^3.12.0";
|
|
41
|
-
} else if (answers.database === "mongodb") {
|
|
42
|
-
deps["mongodb"] = "^6.12.0";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (answers.admin) {
|
|
46
|
-
deps["@kyro-cms/admin"] = "latest";
|
|
47
|
-
deps["@astrojs/node"] = "^9.5.5";
|
|
48
|
-
deps["lucide-react"] = "^0.475.0";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
26
|
const scripts: Record<string, string> = {
|
|
52
|
-
dev: "astro dev",
|
|
53
|
-
build: "astro build",
|
|
54
|
-
preview: "astro preview",
|
|
27
|
+
"dev": "astro dev",
|
|
28
|
+
"build": "astro build",
|
|
29
|
+
"preview": "astro preview",
|
|
55
30
|
};
|
|
56
31
|
|
|
57
|
-
if (answers.auth) {
|
|
58
|
-
scripts["db:bootstrap"] = "kyro auth bootstrap";
|
|
59
|
-
}
|
|
60
|
-
|
|
61
32
|
if (answers.database === "sqlite") {
|
|
62
33
|
scripts["db:generate"] = "kyro generate";
|
|
63
34
|
scripts["db:push"] = "kyro push";
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
|
8
8
|
import { join } from 'path';
|
|
9
9
|
import { execSync } from 'child_process';
|
|
10
10
|
|
|
11
|
-
const VERSION = '0.
|
|
11
|
+
const VERSION = '0.4.0';
|
|
12
12
|
|
|
13
13
|
async function main() {
|
|
14
14
|
logger.intro('create-kyro', VERSION);
|
|
@@ -34,7 +34,7 @@ async function main() {
|
|
|
34
34
|
|
|
35
35
|
logger.step(2, steps.length, steps[1]);
|
|
36
36
|
|
|
37
|
-
const pkg = generatePackageJson(answers
|
|
37
|
+
const pkg = generatePackageJson(answers);
|
|
38
38
|
writeFileSync(
|
|
39
39
|
join(projectDir, 'package.json'),
|
|
40
40
|
formatPackageJson(pkg)
|
|
@@ -85,9 +85,9 @@ async function main() {
|
|
|
85
85
|
console.log(` ${logger ? '\x1b[36m' : ''}npm run dev${logger ? '\x1b[0m' : ''}`);
|
|
86
86
|
console.log('');
|
|
87
87
|
console.log(' Visit http://localhost:4321 to see your app.');
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
console.log(' Visit http://localhost:4321/admin for the admin dashboard.');
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(' The first user to register will be the super admin.');
|
|
91
91
|
console.log('');
|
|
92
92
|
}
|
|
93
93
|
|