@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.
Files changed (40) hide show
  1. package/package.json +34 -33
  2. package/src/index.js +193 -56
  3. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +17 -1
  4. package/templates/base-project/apps/web/src/app/api/error-handler.js +4 -2
  5. package/templates/base-project/apps/web/src/app/api/health/route.js +8 -3
  6. package/templates/base-project/apps/web/src/app/api/posts/[id]/route.js +9 -5
  7. package/templates/base-project/apps/web/src/app/api/posts/route.js +13 -5
  8. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +9 -9
  9. package/templates/base-project/apps/web/src/app/api/users/route.js +6 -6
  10. package/templates/base-project/apps/web/src/lib/auth-middleware.js +18 -1
  11. package/templates/base-project/apps/web/src/{middleware.js → proxy.js} +3 -3
  12. package/templates/base-project/apps/worker/package.json +1 -2
  13. package/templates/base-project/apps/worker/src/index.js +71 -19
  14. package/templates/base-project/docker-compose.yml +3 -6
  15. package/templates/base-project/package.json +16 -1
  16. package/templates/base-project/packages/db/package.json +10 -4
  17. package/templates/base-project/packages/db/prisma.config.ts +2 -2
  18. package/templates/base-project/packages/db/scripts/seed.js +1 -1
  19. package/templates/base-project/packages/db/src/client.js +41 -25
  20. package/templates/base-project/packages/db/src/queries.js +22 -9
  21. package/templates/base-project/packages/db/src/schemas.js +6 -1
  22. package/templates/base-project/turbo.json +17 -1
  23. package/templates/config/package.json +3 -1
  24. package/templates/config/src/app-url.js +71 -0
  25. package/templates/config/src/validate-env.js +104 -0
  26. package/templates/base-project/packages/db/src/generated/prisma/browser.ts +0 -53
  27. package/templates/base-project/packages/db/src/generated/prisma/client.ts +0 -82
  28. package/templates/base-project/packages/db/src/generated/prisma/commonInputTypes.ts +0 -649
  29. package/templates/base-project/packages/db/src/generated/prisma/enums.ts +0 -19
  30. package/templates/base-project/packages/db/src/generated/prisma/internal/class.ts +0 -305
  31. package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespace.ts +0 -1428
  32. package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespaceBrowser.ts +0 -217
  33. package/templates/base-project/packages/db/src/generated/prisma/models/Account.ts +0 -2098
  34. package/templates/base-project/packages/db/src/generated/prisma/models/AuditLog.ts +0 -1805
  35. package/templates/base-project/packages/db/src/generated/prisma/models/Job.ts +0 -1737
  36. package/templates/base-project/packages/db/src/generated/prisma/models/Post.ts +0 -1762
  37. package/templates/base-project/packages/db/src/generated/prisma/models/Session.ts +0 -1738
  38. package/templates/base-project/packages/db/src/generated/prisma/models/User.ts +0 -2298
  39. package/templates/base-project/packages/db/src/generated/prisma/models/VerificationToken.ts +0 -1450
  40. package/templates/base-project/packages/db/src/generated/prisma/models.ts +0 -18
package/package.json CHANGED
@@ -1,34 +1,35 @@
1
1
  {
2
- "name": "@techstream/quark-create-app",
3
- "version": "1.2.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
- "scripts": {
15
- "test": "node test-cli.js",
16
- "test:e2e": "node test-e2e.js",
17
- "test:integration": "node test-integration.js",
18
- "test:all": "node test-all.js"
19
- },
20
- "dependencies": {
21
- "chalk": "^5.6.2",
22
- "commander": "^12.1.0",
23
- "execa": "^9.6.1",
24
- "fs-extra": "^11.3.3",
25
- "prompts": "^2.4.2"
26
- },
27
- "publishConfig": {
28
- "registry": "https://registry.npmjs.org",
29
- "access": "public"
30
- },
31
- "engines": {
32
- "node": ">=24"
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(key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
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 "db" which is always required)
135
- if (packageName === "db" || selectedPackages.includes(packageName)) {
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 (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".next") {
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(`\n\uD83D\uDE80 Creating your new Quark project: ${projectName}\n`),
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
- console.error(chalk.red(`✗ Directory already exists: ${targetDir}`));
214
- process.exit(1);
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 package is always required (already copied from base-project)
237
- // Just update its package.json name
238
- const dbPackageDir = path.join(targetDir, "packages", "db");
239
- const dbPackageJsonPath = path.join(dbPackageDir, "package.json");
240
- const dbPackageJson = await fs.readJSON(dbPackageJsonPath);
241
- dbPackageJson.name = `@${scope}/db`;
242
- await fs.writeFile(
243
- dbPackageJsonPath,
244
- `${JSON.stringify(dbPackageJson, null, 2)}\n`,
245
- );
246
- console.log(chalk.green(` ✓ db (required)`));
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 app dependencies to use correct scope
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
- // Update app package.json files to use correct scope
305
- const appPaths = [
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 appPkgPath of appPaths) {
311
- if (await fs.pathExists(appPkgPath)) {
312
- const appPkg = await fs.readJSON(appPkgPath);
313
- replaceDepsScope(appPkg.dependencies, scope, features);
314
- replaceDepsScope(appPkg.devDependencies, scope, features);
315
- await fs.writeFile(
316
- appPkgPath,
317
- `${JSON.stringify(appPkg, null, 2)}\n`,
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
- # The canonical URL of your application.
358
- # NEXTAUTH_URL, CORS origins, and other URL-dependent settings are derived from this.
359
- # Development: http://localhost:3000
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
- WEB_PORT=3000
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: Generate .env with secure defaults
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=5432
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=6379
526
+ REDIS_PORT=${redisPort}
407
527
 
408
- # --- Mailhog Configuration ---
528
+ # --- Mail Configuration ---
409
529
  MAILHOG_HOST=localhost
410
- MAILHOG_SMTP_PORT=1025
411
- MAILHOG_UI_PORT=8025
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
- WEB_PORT=3000
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
- `\n ⚠️ pnpm install failed: ${installError.message}`,
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
- ` Run 'pnpm install' manually after resolving the issue.`,
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 dev\n`));
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 { hashPassword, validateBody } from "@techstream/quark-core";
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
- console.error("API Error:", error);
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
- console.error("Health check failed:", error);
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: error.message,
40
+ message: "Service health check failed",
36
41
  },
37
42
  { status: 500 },
38
43
  );
@@ -1,4 +1,8 @@
1
- import { UnauthorizedError, validateBody } from "@techstream/quark-core";
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 async function PATCH(request, { params }) {
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 async function DELETE(_request, { params }) {
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 = parseInt(searchParams.get("page") || "1", 10);
11
- const limit = parseInt(searchParams.get("limit") || "10", 10);
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 async function POST(request) {
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 { requireAuth } from "@/lib/auth-middleware";
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 requireAuth();
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 async function PATCH(request, { params }) {
21
+ export const PATCH = withCsrfProtection(async (request, { params }) => {
22
22
  try {
23
- await requireAuth();
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 async function DELETE(_request, { params }) {
39
+ export const DELETE = withCsrfProtection(async (_request, { params }) => {
40
40
  try {
41
- await requireAuth();
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
+ });