@techstream/quark-create-app 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +34 -33
- package/src/index.js +193 -56
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +17 -1
- package/templates/base-project/apps/web/src/app/api/error-handler.js +4 -2
- package/templates/base-project/apps/web/src/app/api/health/route.js +8 -3
- package/templates/base-project/apps/web/src/app/api/posts/[id]/route.js +9 -5
- package/templates/base-project/apps/web/src/app/api/posts/route.js +13 -5
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +9 -9
- package/templates/base-project/apps/web/src/app/api/users/route.js +6 -6
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +18 -1
- package/templates/base-project/apps/web/src/{middleware.js → proxy.js} +3 -3
- package/templates/base-project/apps/worker/package.json +1 -2
- package/templates/base-project/apps/worker/src/index.js +71 -19
- package/templates/base-project/docker-compose.yml +3 -6
- package/templates/base-project/package.json +16 -1
- package/templates/base-project/packages/db/package.json +10 -4
- package/templates/base-project/packages/db/prisma.config.ts +2 -2
- package/templates/base-project/packages/db/scripts/seed.js +1 -1
- package/templates/base-project/packages/db/src/client.js +41 -25
- package/templates/base-project/packages/db/src/queries.js +22 -9
- package/templates/base-project/packages/db/src/schemas.js +6 -1
- package/templates/base-project/turbo.json +17 -1
- package/templates/config/package.json +3 -1
- package/templates/config/src/app-url.js +71 -0
- package/templates/config/src/validate-env.js +104 -0
- package/templates/base-project/packages/db/src/generated/prisma/browser.ts +0 -53
- package/templates/base-project/packages/db/src/generated/prisma/client.ts +0 -82
- package/templates/base-project/packages/db/src/generated/prisma/commonInputTypes.ts +0 -649
- package/templates/base-project/packages/db/src/generated/prisma/enums.ts +0 -19
- package/templates/base-project/packages/db/src/generated/prisma/internal/class.ts +0 -305
- package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespace.ts +0 -1428
- package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespaceBrowser.ts +0 -217
- package/templates/base-project/packages/db/src/generated/prisma/models/Account.ts +0 -2098
- package/templates/base-project/packages/db/src/generated/prisma/models/AuditLog.ts +0 -1805
- package/templates/base-project/packages/db/src/generated/prisma/models/Job.ts +0 -1737
- package/templates/base-project/packages/db/src/generated/prisma/models/Post.ts +0 -1762
- package/templates/base-project/packages/db/src/generated/prisma/models/Session.ts +0 -1738
- package/templates/base-project/packages/db/src/generated/prisma/models/User.ts +0 -2298
- package/templates/base-project/packages/db/src/generated/prisma/models/VerificationToken.ts +0 -1450
- package/templates/base-project/packages/db/src/generated/prisma/models.ts +0 -18
package/package.json
CHANGED
|
@@ -1,34 +1,35 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
2
|
+
"name": "@techstream/quark-create-app",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"quark-create-app": "src/index.js",
|
|
7
|
+
"create-quark-app": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"templates",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"chalk": "^5.6.2",
|
|
16
|
+
"commander": "^12.1.0",
|
|
17
|
+
"execa": "^9.6.1",
|
|
18
|
+
"fs-extra": "^11.3.3",
|
|
19
|
+
"prompts": "^2.4.2"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"registry": "https://registry.npmjs.org",
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22"
|
|
27
|
+
},
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node test-cli.js",
|
|
31
|
+
"test:e2e": "node test-e2e.js",
|
|
32
|
+
"test:integration": "node test-integration.js",
|
|
33
|
+
"test:all": "node test-all.js"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
+
import net from "node:net";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import chalk from "chalk";
|
|
@@ -47,6 +48,48 @@ function generateSecurePassword(length = 24) {
|
|
|
47
48
|
return result;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Check if a TCP port is available on localhost.
|
|
53
|
+
* Uses a connect test (not bind) to reliably detect Docker-bound ports on macOS.
|
|
54
|
+
* @param {number} port
|
|
55
|
+
* @returns {Promise<boolean>}
|
|
56
|
+
*/
|
|
57
|
+
function isPortAvailable(port) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const socket = new net.Socket();
|
|
60
|
+
socket.setTimeout(500);
|
|
61
|
+
socket.once("connect", () => {
|
|
62
|
+
socket.destroy();
|
|
63
|
+
resolve(false); // something is listening — port is in use
|
|
64
|
+
});
|
|
65
|
+
socket.once("error", () => {
|
|
66
|
+
socket.destroy();
|
|
67
|
+
resolve(true); // ECONNREFUSED — port is free
|
|
68
|
+
});
|
|
69
|
+
socket.once("timeout", () => {
|
|
70
|
+
socket.destroy();
|
|
71
|
+
resolve(true); // no response — port is free
|
|
72
|
+
});
|
|
73
|
+
socket.connect(port, "127.0.0.1");
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Find the next available port starting from a given port
|
|
79
|
+
* @param {number} startPort
|
|
80
|
+
* @param {number} [maxAttempts=20]
|
|
81
|
+
* @returns {Promise<number>}
|
|
82
|
+
*/
|
|
83
|
+
async function findAvailablePort(startPort, maxAttempts = 20) {
|
|
84
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
85
|
+
const port = startPort + i;
|
|
86
|
+
if (await isPortAvailable(port)) {
|
|
87
|
+
return port;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return startPort; // fallback to default if all checked ports are busy
|
|
91
|
+
}
|
|
92
|
+
|
|
50
93
|
/**
|
|
51
94
|
* Copy a template directory to the target location, with variable substitution
|
|
52
95
|
*/
|
|
@@ -67,7 +110,10 @@ async function copyTemplate(templateName, targetDir, variables = {}) {
|
|
|
67
110
|
let content = await fs.readFile(packageJsonPath, "utf-8");
|
|
68
111
|
|
|
69
112
|
for (const [key, value] of Object.entries(variables)) {
|
|
70
|
-
const pattern = new RegExp(
|
|
113
|
+
const pattern = new RegExp(
|
|
114
|
+
key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
|
115
|
+
"g",
|
|
116
|
+
);
|
|
71
117
|
content = content.replace(pattern, value);
|
|
72
118
|
}
|
|
73
119
|
|
|
@@ -131,8 +177,12 @@ function replaceDepsScope(deps, scope, selectedPackages) {
|
|
|
131
177
|
if (key.startsWith("@techstream/quark-") && value === "workspace:*") {
|
|
132
178
|
const packageName = key.replace("@techstream/quark-", "");
|
|
133
179
|
delete deps[key];
|
|
134
|
-
// Only keep the dep if the package was selected (or is
|
|
135
|
-
if (
|
|
180
|
+
// Only keep the dep if the package was selected (or is always required)
|
|
181
|
+
if (
|
|
182
|
+
packageName === "db" ||
|
|
183
|
+
packageName === "config" ||
|
|
184
|
+
selectedPackages.includes(packageName)
|
|
185
|
+
) {
|
|
136
186
|
deps[`@${scope}/${packageName}`] = value;
|
|
137
187
|
}
|
|
138
188
|
}
|
|
@@ -151,7 +201,11 @@ async function replaceImportsInSourceFiles(dir, scope) {
|
|
|
151
201
|
for (const entry of entries) {
|
|
152
202
|
const fullPath = path.join(dir, entry.name);
|
|
153
203
|
|
|
154
|
-
if (
|
|
204
|
+
if (
|
|
205
|
+
entry.isDirectory() &&
|
|
206
|
+
entry.name !== "node_modules" &&
|
|
207
|
+
entry.name !== ".next"
|
|
208
|
+
) {
|
|
155
209
|
await replaceImportsInSourceFiles(fullPath, scope);
|
|
156
210
|
} else if (entry.isFile() && /\.(js|ts|jsx|tsx|mjs)$/.test(entry.name)) {
|
|
157
211
|
let content = await fs.readFile(fullPath, "utf-8");
|
|
@@ -202,7 +256,9 @@ program
|
|
|
202
256
|
.argument("<project-name>", "Name of the project to create")
|
|
203
257
|
.action(async (projectName) => {
|
|
204
258
|
console.log(
|
|
205
|
-
chalk.blue.bold(
|
|
259
|
+
chalk.blue.bold(
|
|
260
|
+
`\n\uD83D\uDE80 Creating your new Quark project: ${projectName}\n`,
|
|
261
|
+
),
|
|
206
262
|
);
|
|
207
263
|
|
|
208
264
|
const targetDir = validateProjectName(projectName);
|
|
@@ -210,8 +266,30 @@ program
|
|
|
210
266
|
|
|
211
267
|
// Check if directory already exists
|
|
212
268
|
if (await fs.pathExists(targetDir)) {
|
|
213
|
-
|
|
214
|
-
|
|
269
|
+
const { overwrite } = await prompts({
|
|
270
|
+
type: "confirm",
|
|
271
|
+
name: "overwrite",
|
|
272
|
+
message: `Directory "${projectName}" already exists. Remove it and recreate?`,
|
|
273
|
+
initial: false,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (!overwrite) {
|
|
277
|
+
console.log(chalk.yellow("Aborted."));
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Clean up Docker resources (volumes hold old credentials)
|
|
282
|
+
try {
|
|
283
|
+
await execa("docker", ["compose", "down", "-v"], {
|
|
284
|
+
cwd: targetDir,
|
|
285
|
+
stdio: "ignore",
|
|
286
|
+
});
|
|
287
|
+
console.log(chalk.green(" ✓ Cleaned up Docker volumes"));
|
|
288
|
+
} catch {
|
|
289
|
+
// No docker-compose file or Docker not running — fine
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
await fs.remove(targetDir);
|
|
215
293
|
}
|
|
216
294
|
|
|
217
295
|
try {
|
|
@@ -233,25 +311,33 @@ program
|
|
|
233
311
|
// Step 4: Copy required packages (always included)
|
|
234
312
|
console.log(chalk.cyan("\n 📦 Setting up required packages..."));
|
|
235
313
|
|
|
236
|
-
// Database
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
314
|
+
// Database and config packages are always required
|
|
315
|
+
const requiredPackages = ["db", "config"];
|
|
316
|
+
for (const reqPkg of requiredPackages) {
|
|
317
|
+
const pkgDir = path.join(targetDir, "packages", reqPkg);
|
|
318
|
+
// db is already copied from base-project; config needs to be copied from its template
|
|
319
|
+
if (!(await fs.pathExists(pkgDir))) {
|
|
320
|
+
await fs.ensureDir(pkgDir);
|
|
321
|
+
await copyTemplate(reqPkg, pkgDir);
|
|
322
|
+
}
|
|
323
|
+
const pkgJsonPath = path.join(pkgDir, "package.json");
|
|
324
|
+
const pkgJson = await fs.readJSON(pkgJsonPath);
|
|
325
|
+
pkgJson.name = `@${scope}/${reqPkg}`;
|
|
326
|
+
await fs.writeFile(
|
|
327
|
+
pkgJsonPath,
|
|
328
|
+
`${JSON.stringify(pkgJson, null, 2)}\n`,
|
|
329
|
+
);
|
|
330
|
+
console.log(chalk.green(` ✓ ${reqPkg} (required)`));
|
|
331
|
+
}
|
|
247
332
|
|
|
248
333
|
// Step 5: Ask which optional features to eject
|
|
249
|
-
console.log(chalk.cyan("\n 🎯 Configuring optional features
|
|
334
|
+
console.log(chalk.cyan("\n 🎯 Configuring optional features...\n"));
|
|
250
335
|
const response = await prompts([
|
|
251
336
|
{
|
|
252
337
|
type: "multiselect",
|
|
253
338
|
name: "features",
|
|
254
339
|
message: "Which optional packages would you like to include?",
|
|
340
|
+
instructions: false,
|
|
255
341
|
choices: [
|
|
256
342
|
{
|
|
257
343
|
title: "UI Components (packages/ui)",
|
|
@@ -263,11 +349,6 @@ program
|
|
|
263
349
|
value: "jobs",
|
|
264
350
|
selected: true,
|
|
265
351
|
},
|
|
266
|
-
{
|
|
267
|
-
title: "Configuration (packages/config)",
|
|
268
|
-
value: "config",
|
|
269
|
-
selected: false,
|
|
270
|
-
},
|
|
271
352
|
],
|
|
272
353
|
},
|
|
273
354
|
]);
|
|
@@ -298,27 +379,44 @@ program
|
|
|
298
379
|
}
|
|
299
380
|
}
|
|
300
381
|
|
|
301
|
-
// Step 7: Update
|
|
382
|
+
// Step 7: Update all package.json dependencies to use correct scope
|
|
302
383
|
console.log(chalk.cyan("\n 🔧 Updating app dependencies..."));
|
|
303
384
|
|
|
304
|
-
//
|
|
305
|
-
const
|
|
385
|
+
// Collect all package.json files that need scope replacement (apps + packages)
|
|
386
|
+
const allPkgPaths = [
|
|
306
387
|
path.join(targetDir, "apps", "web", "package.json"),
|
|
307
388
|
path.join(targetDir, "apps", "worker", "package.json"),
|
|
389
|
+
// Also update cross-dependencies in scaffolded packages (e.g. db → config)
|
|
390
|
+
...["db", ...features].map((pkg) =>
|
|
391
|
+
path.join(targetDir, "packages", pkg, "package.json"),
|
|
392
|
+
),
|
|
308
393
|
];
|
|
309
394
|
|
|
310
|
-
for (const
|
|
311
|
-
if (await fs.pathExists(
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
);
|
|
395
|
+
for (const pkgPath of allPkgPaths) {
|
|
396
|
+
if (await fs.pathExists(pkgPath)) {
|
|
397
|
+
const pkg = await fs.readJSON(pkgPath);
|
|
398
|
+
// Rename package name if it uses @quark/ prefix
|
|
399
|
+
if (pkg.name?.startsWith("@quark/")) {
|
|
400
|
+
const shortName = pkg.name.replace("@quark/", "");
|
|
401
|
+
pkg.name = `@${scope}/${shortName}`;
|
|
402
|
+
}
|
|
403
|
+
replaceDepsScope(pkg.dependencies, scope, features);
|
|
404
|
+
replaceDepsScope(pkg.devDependencies, scope, features);
|
|
405
|
+
await fs.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
319
406
|
}
|
|
320
407
|
}
|
|
321
408
|
|
|
409
|
+
// Also rename root package.json
|
|
410
|
+
const rootPkgPath = path.join(targetDir, "package.json");
|
|
411
|
+
if (await fs.pathExists(rootPkgPath)) {
|
|
412
|
+
const rootPkg = await fs.readJSON(rootPkgPath);
|
|
413
|
+
rootPkg.name = `@${scope}/root`;
|
|
414
|
+
await fs.writeFile(
|
|
415
|
+
rootPkgPath,
|
|
416
|
+
`${JSON.stringify(rootPkg, null, 2)}\n`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
322
420
|
console.log(chalk.green(` ✓ App dependencies updated`));
|
|
323
421
|
|
|
324
422
|
// Step 7b: Replace workspace package imports in source files
|
|
@@ -354,11 +452,9 @@ MAILHOG_UI_PORT=8025
|
|
|
354
452
|
# MAILHOG_SMTP_URL="smtp://localhost:1025"
|
|
355
453
|
|
|
356
454
|
# --- Application URL ---
|
|
357
|
-
#
|
|
358
|
-
#
|
|
359
|
-
#
|
|
360
|
-
# Production: https://yourdomain.com
|
|
361
|
-
APP_URL=http://localhost:3000
|
|
455
|
+
# In development, APP_URL is derived automatically from PORT — no need to set it.
|
|
456
|
+
# In production, set this to your real domain:
|
|
457
|
+
# APP_URL=https://yourdomain.com
|
|
362
458
|
|
|
363
459
|
# --- NextAuth Configuration ---
|
|
364
460
|
# ⚠️ CRITICAL: Generate a secure secret with: openssl rand -base64 32
|
|
@@ -375,7 +471,7 @@ NEXTAUTH_SECRET=CHANGE_ME_TO_STRONG_SECRET
|
|
|
375
471
|
# GOOGLE_CLIENT_SECRET=your_google_client_secret
|
|
376
472
|
|
|
377
473
|
# --- Web App Configuration ---
|
|
378
|
-
|
|
474
|
+
PORT=3000
|
|
379
475
|
|
|
380
476
|
# --- Worker Configuration ---
|
|
381
477
|
WORKER_CONCURRENCY=5
|
|
@@ -386,7 +482,31 @@ WORKER_CONCURRENCY=5
|
|
|
386
482
|
);
|
|
387
483
|
console.log(chalk.green(` ✓ .env.example`));
|
|
388
484
|
|
|
389
|
-
// Step 9:
|
|
485
|
+
// Step 9: Find available ports and generate .env
|
|
486
|
+
console.log(chalk.cyan("\n 🔌 Checking port availability..."));
|
|
487
|
+
const postgresPort = await findAvailablePort(5432);
|
|
488
|
+
const redisPort = await findAvailablePort(6379);
|
|
489
|
+
const mailSmtpPort = await findAvailablePort(1025);
|
|
490
|
+
const mailUiPort = await findAvailablePort(8025);
|
|
491
|
+
const webPort = await findAvailablePort(3000);
|
|
492
|
+
|
|
493
|
+
const portChanges = [];
|
|
494
|
+
if (postgresPort !== 5432)
|
|
495
|
+
portChanges.push(`PostgreSQL: ${postgresPort}`);
|
|
496
|
+
if (redisPort !== 6379) portChanges.push(`Redis: ${redisPort}`);
|
|
497
|
+
if (mailSmtpPort !== 1025) portChanges.push(`Mail SMTP: ${mailSmtpPort}`);
|
|
498
|
+
if (mailUiPort !== 8025) portChanges.push(`Mail UI: ${mailUiPort}`);
|
|
499
|
+
if (webPort !== 3000) portChanges.push(`Web: ${webPort}`);
|
|
500
|
+
|
|
501
|
+
if (portChanges.length > 0) {
|
|
502
|
+
console.log(chalk.yellow(` ⚡ Ports adjusted to avoid conflicts:`));
|
|
503
|
+
for (const change of portChanges) {
|
|
504
|
+
console.log(chalk.yellow(` • ${change}`));
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
console.log(chalk.green(` ✓ All default ports available`));
|
|
508
|
+
}
|
|
509
|
+
|
|
390
510
|
console.log(chalk.cyan("\n 🔑 Generating secure environment file..."));
|
|
391
511
|
|
|
392
512
|
// Generate secure random values
|
|
@@ -396,28 +516,27 @@ WORKER_CONCURRENCY=5
|
|
|
396
516
|
// Create .env with auto-generated secure values
|
|
397
517
|
const envContent = `# --- Database Configuration ---
|
|
398
518
|
POSTGRES_HOST=localhost
|
|
399
|
-
POSTGRES_PORT
|
|
519
|
+
POSTGRES_PORT=${postgresPort}
|
|
400
520
|
POSTGRES_USER=quark_user
|
|
401
521
|
POSTGRES_PASSWORD=${dbPassword}
|
|
402
522
|
POSTGRES_DB=${scope}_dev
|
|
403
523
|
|
|
404
524
|
# --- Redis Configuration ---
|
|
405
525
|
REDIS_HOST=localhost
|
|
406
|
-
REDIS_PORT
|
|
526
|
+
REDIS_PORT=${redisPort}
|
|
407
527
|
|
|
408
|
-
# ---
|
|
528
|
+
# --- Mail Configuration ---
|
|
409
529
|
MAILHOG_HOST=localhost
|
|
410
|
-
MAILHOG_SMTP_PORT
|
|
411
|
-
MAILHOG_UI_PORT
|
|
530
|
+
MAILHOG_SMTP_PORT=${mailSmtpPort}
|
|
531
|
+
MAILHOG_UI_PORT=${mailUiPort}
|
|
412
532
|
|
|
413
533
|
# --- NextAuth Configuration ---
|
|
414
534
|
NEXTAUTH_SECRET=${nextAuthSecret}
|
|
415
535
|
|
|
416
|
-
# --- Application URL ---
|
|
417
|
-
APP_URL=http://localhost:3000
|
|
418
|
-
|
|
419
536
|
# --- Web App Configuration ---
|
|
420
|
-
|
|
537
|
+
# APP_URL is derived from PORT automatically in development.
|
|
538
|
+
# In production, set APP_URL explicitly in your environment.
|
|
539
|
+
PORT=${webPort}
|
|
421
540
|
|
|
422
541
|
# --- Worker Configuration ---
|
|
423
542
|
WORKER_CONCURRENCY=5
|
|
@@ -432,7 +551,7 @@ WORKER_CONCURRENCY=5
|
|
|
432
551
|
quarkVersion: process.env.QUARK_VERSION || "latest",
|
|
433
552
|
quarkSourcePath: process.env.QUARK_SOURCE_PATH || "../../quark",
|
|
434
553
|
scaffoldedDate: new Date().toISOString(),
|
|
435
|
-
requiredPackages: ["db"],
|
|
554
|
+
requiredPackages: ["db", "config"],
|
|
436
555
|
packages: features,
|
|
437
556
|
};
|
|
438
557
|
await fs.writeFile(
|
|
@@ -457,16 +576,33 @@ WORKER_CONCURRENCY=5
|
|
|
457
576
|
});
|
|
458
577
|
console.log(chalk.green(`\n ✓ Dependencies installed`));
|
|
459
578
|
} catch (installError) {
|
|
579
|
+
console.warn(
|
|
580
|
+
chalk.yellow(`\n ⚠️ pnpm install failed: ${installError.message}`),
|
|
581
|
+
);
|
|
460
582
|
console.warn(
|
|
461
583
|
chalk.yellow(
|
|
462
|
-
|
|
584
|
+
` Run 'pnpm install' manually after resolving the issue.`,
|
|
463
585
|
),
|
|
464
586
|
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Step 13: Generate Prisma client
|
|
590
|
+
console.log(chalk.cyan("\n 🗄️ Generating Prisma client..."));
|
|
591
|
+
try {
|
|
592
|
+
await execa("pnpm", ["--filter", "db", "db:generate"], {
|
|
593
|
+
cwd: targetDir,
|
|
594
|
+
stdio: "inherit",
|
|
595
|
+
});
|
|
596
|
+
console.log(chalk.green(` ✓ Prisma client generated`));
|
|
597
|
+
} catch (generateError) {
|
|
465
598
|
console.warn(
|
|
466
599
|
chalk.yellow(
|
|
467
|
-
|
|
600
|
+
`\n ⚠️ Prisma generate failed: ${generateError.message}`,
|
|
468
601
|
),
|
|
469
602
|
);
|
|
603
|
+
console.warn(
|
|
604
|
+
chalk.yellow(` Run 'pnpm --filter db db:generate' manually.`),
|
|
605
|
+
);
|
|
470
606
|
}
|
|
471
607
|
|
|
472
608
|
// Success message
|
|
@@ -480,7 +616,8 @@ WORKER_CONCURRENCY=5
|
|
|
480
616
|
console.log(chalk.cyan("Next steps:"));
|
|
481
617
|
console.log(chalk.white(` 1. cd ${projectName}`));
|
|
482
618
|
console.log(chalk.white(` 2. docker compose up -d`));
|
|
483
|
-
console.log(chalk.white(` 3. pnpm
|
|
619
|
+
console.log(chalk.white(` 3. pnpm --filter db db:push`));
|
|
620
|
+
console.log(chalk.white(` 4. pnpm dev\n`));
|
|
484
621
|
|
|
485
622
|
console.log(chalk.cyan("Important:"));
|
|
486
623
|
console.log(
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createQueue,
|
|
3
|
+
hashPassword,
|
|
4
|
+
validateBody,
|
|
5
|
+
} from "@techstream/quark-core";
|
|
2
6
|
import { user, userRegisterSchema } from "@techstream/quark-db";
|
|
7
|
+
import { JOB_NAMES, JOB_QUEUES } from "@techstream/quark-jobs";
|
|
3
8
|
import { NextResponse } from "next/server";
|
|
4
9
|
import { handleError } from "../../error-handler";
|
|
5
10
|
|
|
@@ -29,6 +34,17 @@ export async function POST(request) {
|
|
|
29
34
|
password: hashedPassword,
|
|
30
35
|
});
|
|
31
36
|
|
|
37
|
+
// Enqueue welcome email (fire-and-forget)
|
|
38
|
+
try {
|
|
39
|
+
const emailQueue = createQueue(JOB_QUEUES.EMAIL);
|
|
40
|
+
await emailQueue.add(JOB_NAMES.SEND_WELCOME_EMAIL, {
|
|
41
|
+
userId: newUser.id,
|
|
42
|
+
});
|
|
43
|
+
} catch (emailError) {
|
|
44
|
+
// Don't fail registration if email enqueue fails
|
|
45
|
+
console.error("Failed to enqueue welcome email:", emailError);
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
// Don't return the password
|
|
33
49
|
const { password: _, ...safeUser } = newUser;
|
|
34
50
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { AppError } from "@techstream/quark-core";
|
|
1
|
+
import { AppError, createLogger } from "@techstream/quark-core";
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
3
|
|
|
4
|
+
const logger = createLogger("api");
|
|
5
|
+
|
|
4
6
|
export function handleError(error) {
|
|
5
|
-
|
|
7
|
+
logger.error("API Error", { error: error.message, stack: error.stack });
|
|
6
8
|
|
|
7
9
|
if (error instanceof AppError) {
|
|
8
10
|
return NextResponse.json(error.toJSON(), { status: error.statusCode });
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* Times out after 5 seconds to prevent hanging.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { pingRedis } from "@techstream/quark-core";
|
|
7
|
+
import { createLogger, pingRedis } from "@techstream/quark-core";
|
|
8
8
|
import { prisma } from "@techstream/quark-db";
|
|
9
9
|
import { NextResponse } from "next/server";
|
|
10
10
|
|
|
11
|
+
const logger = createLogger("health");
|
|
12
|
+
|
|
11
13
|
/** Overall timeout for the health check (ms). */
|
|
12
14
|
const HEALTH_CHECK_TIMEOUT = 5000;
|
|
13
15
|
|
|
@@ -27,12 +29,15 @@ export async function GET() {
|
|
|
27
29
|
status: result.status === "ok" ? 200 : 503,
|
|
28
30
|
});
|
|
29
31
|
} catch (error) {
|
|
30
|
-
|
|
32
|
+
logger.error("Health check failed", {
|
|
33
|
+
error: error.message,
|
|
34
|
+
stack: error.stack,
|
|
35
|
+
});
|
|
31
36
|
return NextResponse.json(
|
|
32
37
|
{
|
|
33
38
|
status: "error",
|
|
34
39
|
timestamp: new Date().toISOString(),
|
|
35
|
-
message:
|
|
40
|
+
message: "Service health check failed",
|
|
36
41
|
},
|
|
37
42
|
{ status: 500 },
|
|
38
43
|
);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
UnauthorizedError,
|
|
3
|
+
validateBody,
|
|
4
|
+
withCsrfProtection,
|
|
5
|
+
} from "@techstream/quark-core";
|
|
2
6
|
import { post, postUpdateSchema } from "@techstream/quark-db";
|
|
3
7
|
import { NextResponse } from "next/server";
|
|
4
8
|
import { requireAuth } from "@/lib/auth-middleware";
|
|
@@ -17,7 +21,7 @@ export async function GET(_request, { params }) {
|
|
|
17
21
|
}
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
export
|
|
24
|
+
export const PATCH = withCsrfProtection(async (request, { params }) => {
|
|
21
25
|
try {
|
|
22
26
|
const session = await requireAuth();
|
|
23
27
|
const { id } = await params;
|
|
@@ -37,9 +41,9 @@ export async function PATCH(request, { params }) {
|
|
|
37
41
|
} catch (error) {
|
|
38
42
|
return handleError(error);
|
|
39
43
|
}
|
|
40
|
-
}
|
|
44
|
+
});
|
|
41
45
|
|
|
42
|
-
export
|
|
46
|
+
export const DELETE = withCsrfProtection(async (_request, { params }) => {
|
|
43
47
|
try {
|
|
44
48
|
const session = await requireAuth();
|
|
45
49
|
const { id } = await params;
|
|
@@ -58,4 +62,4 @@ export async function DELETE(_request, { params }) {
|
|
|
58
62
|
} catch (error) {
|
|
59
63
|
return handleError(error);
|
|
60
64
|
}
|
|
61
|
-
}
|
|
65
|
+
});
|
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import { validateBody } from "@techstream/quark-core";
|
|
1
|
+
import { validateBody, withCsrfProtection } from "@techstream/quark-core";
|
|
2
2
|
import { post, postCreateSchema } from "@techstream/quark-db";
|
|
3
3
|
import { NextResponse } from "next/server";
|
|
4
|
+
import { z } from "zod";
|
|
4
5
|
import { requireAuth } from "@/lib/auth-middleware";
|
|
5
6
|
import { handleError } from "../error-handler";
|
|
6
7
|
|
|
8
|
+
const paginationSchema = z.object({
|
|
9
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
10
|
+
limit: z.coerce.number().int().min(1).max(100).default(10),
|
|
11
|
+
});
|
|
12
|
+
|
|
7
13
|
export async function GET(request) {
|
|
8
14
|
try {
|
|
9
15
|
const { searchParams } = new URL(request.url);
|
|
10
|
-
const page =
|
|
11
|
-
|
|
16
|
+
const { page, limit } = paginationSchema.parse({
|
|
17
|
+
page: searchParams.get("page") ?? undefined,
|
|
18
|
+
limit: searchParams.get("limit") ?? undefined,
|
|
19
|
+
});
|
|
12
20
|
const skip = (page - 1) * limit;
|
|
13
21
|
|
|
14
22
|
const posts = await post.findAll({ skip, take: limit });
|
|
@@ -18,7 +26,7 @@ export async function GET(request) {
|
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
export
|
|
29
|
+
export const POST = withCsrfProtection(async (request) => {
|
|
22
30
|
try {
|
|
23
31
|
const session = await requireAuth();
|
|
24
32
|
const data = await validateBody(request, postCreateSchema);
|
|
@@ -31,4 +39,4 @@ export async function POST(request) {
|
|
|
31
39
|
} catch (error) {
|
|
32
40
|
return handleError(error);
|
|
33
41
|
}
|
|
34
|
-
}
|
|
42
|
+
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { validateBody } from "@techstream/quark-core";
|
|
1
|
+
import { validateBody, withCsrfProtection } from "@techstream/quark-core";
|
|
2
2
|
import { user, userUpdateSchema } from "@techstream/quark-db";
|
|
3
3
|
import { NextResponse } from "next/server";
|
|
4
|
-
import {
|
|
4
|
+
import { requireRole } from "@/lib/auth-middleware";
|
|
5
5
|
import { handleError } from "../../error-handler";
|
|
6
6
|
|
|
7
7
|
export async function GET(_request, { params }) {
|
|
8
8
|
try {
|
|
9
|
-
await
|
|
9
|
+
await requireRole("admin");
|
|
10
10
|
const { id } = await params;
|
|
11
11
|
const foundUser = await user.findById(id);
|
|
12
12
|
if (!foundUser) {
|
|
@@ -18,9 +18,9 @@ export async function GET(_request, { params }) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export
|
|
21
|
+
export const PATCH = withCsrfProtection(async (request, { params }) => {
|
|
22
22
|
try {
|
|
23
|
-
await
|
|
23
|
+
await requireRole("admin");
|
|
24
24
|
const { id } = await params;
|
|
25
25
|
|
|
26
26
|
const existingUser = await user.findById(id);
|
|
@@ -34,11 +34,11 @@ export async function PATCH(request, { params }) {
|
|
|
34
34
|
} catch (error) {
|
|
35
35
|
return handleError(error);
|
|
36
36
|
}
|
|
37
|
-
}
|
|
37
|
+
});
|
|
38
38
|
|
|
39
|
-
export
|
|
39
|
+
export const DELETE = withCsrfProtection(async (_request, { params }) => {
|
|
40
40
|
try {
|
|
41
|
-
await
|
|
41
|
+
await requireRole("admin");
|
|
42
42
|
const { id } = await params;
|
|
43
43
|
|
|
44
44
|
const existingUser = await user.findById(id);
|
|
@@ -51,4 +51,4 @@ export async function DELETE(_request, { params }) {
|
|
|
51
51
|
} catch (error) {
|
|
52
52
|
return handleError(error);
|
|
53
53
|
}
|
|
54
|
-
}
|
|
54
|
+
});
|