servcraft 0.1.6 → 0.1.7

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.
@@ -28,7 +28,7 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
28
28
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
29
29
 
30
30
  // src/cli/index.ts
31
- var import_commander5 = require("commander");
31
+ var import_commander6 = require("commander");
32
32
 
33
33
  // src/cli/commands/init.ts
34
34
  var import_commander = require("commander");
@@ -100,7 +100,7 @@ function getModulesDir() {
100
100
  }
101
101
 
102
102
  // src/cli/commands/init.ts
103
- var initCommand = new import_commander.Command("init").alias("new").description("Initialize a new Servcraft project").argument("[name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").option("--ts, --typescript", "Use TypeScript (default)").option("--js, --javascript", "Use JavaScript").option("--db <database>", "Database type (postgresql, mysql, sqlite, mongodb, none)").action(
103
+ var initCommand = new import_commander.Command("init").alias("new").description("Initialize a new Servcraft project").argument("[name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").option("--ts, --typescript", "Use TypeScript (default)").option("--js, --javascript", "Use JavaScript").option("--esm", "Use ES Modules (import/export) - default").option("--cjs, --commonjs", "Use CommonJS (require/module.exports)").option("--db <database>", "Database type (postgresql, mysql, sqlite, mongodb, none)").action(
104
104
  async (name, cmdOptions) => {
105
105
  console.log(
106
106
  import_chalk2.default.blue(`
@@ -117,6 +117,7 @@ var initCommand = new import_commander.Command("init").alias("new").description(
117
117
  options = {
118
118
  name: name || "my-servcraft-app",
119
119
  language: cmdOptions.javascript ? "javascript" : "typescript",
120
+ moduleSystem: cmdOptions.commonjs ? "commonjs" : "esm",
120
121
  database: db,
121
122
  orm: db === "mongodb" ? "mongoose" : db === "none" ? "none" : "prisma",
122
123
  validator: "zod",
@@ -146,6 +147,16 @@ var initCommand = new import_commander.Command("init").alias("new").description(
146
147
  ],
147
148
  default: "typescript"
148
149
  },
150
+ {
151
+ type: "list",
152
+ name: "moduleSystem",
153
+ message: "Select module system:",
154
+ choices: [
155
+ { name: "ESM (import/export) - Recommended", value: "esm" },
156
+ { name: "CommonJS (require/module.exports)", value: "commonjs" }
157
+ ],
158
+ default: "esm"
159
+ },
149
160
  {
150
161
  type: "list",
151
162
  name: "database",
@@ -208,10 +219,10 @@ var initCommand = new import_commander.Command("init").alias("new").description(
208
219
  JSON.stringify(packageJson, null, 2)
209
220
  );
210
221
  if (options.language === "typescript") {
211
- await writeFile(import_path2.default.join(projectDir, "tsconfig.json"), generateTsConfig());
212
- await writeFile(import_path2.default.join(projectDir, "tsup.config.ts"), generateTsupConfig());
222
+ await writeFile(import_path2.default.join(projectDir, "tsconfig.json"), generateTsConfig(options));
223
+ await writeFile(import_path2.default.join(projectDir, "tsup.config.ts"), generateTsupConfig(options));
213
224
  } else {
214
- await writeFile(import_path2.default.join(projectDir, "jsconfig.json"), generateJsConfig());
225
+ await writeFile(import_path2.default.join(projectDir, "jsconfig.json"), generateJsConfig(options));
215
226
  }
216
227
  await writeFile(import_path2.default.join(projectDir, ".env.example"), generateEnvExample(options));
217
228
  await writeFile(import_path2.default.join(projectDir, ".env"), generateEnvExample(options));
@@ -221,7 +232,7 @@ var initCommand = new import_commander.Command("init").alias("new").description(
221
232
  import_path2.default.join(projectDir, "docker-compose.yml"),
222
233
  generateDockerCompose(options)
223
234
  );
224
- const ext = options.language === "typescript" ? "ts" : "js";
235
+ const ext = options.language === "typescript" ? "ts" : options.moduleSystem === "esm" ? "js" : "cjs";
225
236
  const dirs = [
226
237
  "src/core",
227
238
  "src/config",
@@ -250,6 +261,22 @@ var initCommand = new import_commander.Command("init").alias("new").description(
250
261
  import_path2.default.join(projectDir, `src/core/logger.${ext}`),
251
262
  generateLoggerFile(options)
252
263
  );
264
+ await writeFile(
265
+ import_path2.default.join(projectDir, `src/config/index.${ext}`),
266
+ generateConfigFile(options)
267
+ );
268
+ await writeFile(
269
+ import_path2.default.join(projectDir, `src/middleware/index.${ext}`),
270
+ generateMiddlewareFile(options)
271
+ );
272
+ await writeFile(
273
+ import_path2.default.join(projectDir, `src/utils/index.${ext}`),
274
+ generateUtilsFile(options)
275
+ );
276
+ await writeFile(
277
+ import_path2.default.join(projectDir, `src/types/index.${ext}`),
278
+ generateTypesFile(options)
279
+ );
253
280
  if (options.orm === "prisma") {
254
281
  await writeFile(
255
282
  import_path2.default.join(projectDir, "prisma/schema.prisma"),
@@ -311,18 +338,35 @@ var initCommand = new import_commander.Command("init").alias("new").description(
311
338
  );
312
339
  function generatePackageJson(options) {
313
340
  const isTS = options.language === "typescript";
341
+ const isESM = options.moduleSystem === "esm";
342
+ let devCommand;
343
+ if (isTS) {
344
+ devCommand = "tsx watch src/index.ts";
345
+ } else if (isESM) {
346
+ devCommand = "node --watch src/index.js";
347
+ } else {
348
+ devCommand = "node --watch src/index.cjs";
349
+ }
350
+ let startCommand;
351
+ if (isTS) {
352
+ startCommand = isESM ? "node dist/index.js" : "node dist/index.cjs";
353
+ } else if (isESM) {
354
+ startCommand = "node src/index.js";
355
+ } else {
356
+ startCommand = "node src/index.cjs";
357
+ }
314
358
  const pkg = {
315
359
  name: options.name,
316
360
  version: "0.1.0",
317
361
  description: "A Servcraft application",
318
- main: isTS ? "dist/index.js" : "src/index.js",
319
- type: "module",
362
+ main: isTS ? isESM ? "dist/index.js" : "dist/index.cjs" : isESM ? "src/index.js" : "src/index.cjs",
363
+ ...isESM && { type: "module" },
320
364
  scripts: {
321
- dev: isTS ? "tsx watch src/index.ts" : "node --watch src/index.js",
365
+ dev: devCommand,
322
366
  build: isTS ? "tsup" : 'echo "No build needed for JS"',
323
- start: isTS ? "node dist/index.js" : "node src/index.js",
367
+ start: startCommand,
324
368
  test: "vitest",
325
- lint: isTS ? "eslint src --ext .ts" : "eslint src --ext .js"
369
+ lint: isTS ? "eslint src --ext .ts" : "eslint src --ext .js,.cjs"
326
370
  },
327
371
  dependencies: {
328
372
  fastify: "^4.28.1",
@@ -384,13 +428,14 @@ function generatePackageJson(options) {
384
428
  }
385
429
  return pkg;
386
430
  }
387
- function generateTsConfig() {
431
+ function generateTsConfig(options) {
432
+ const isESM = options.moduleSystem === "esm";
388
433
  return JSON.stringify(
389
434
  {
390
435
  compilerOptions: {
391
436
  target: "ES2022",
392
- module: "NodeNext",
393
- moduleResolution: "NodeNext",
437
+ module: isESM ? "NodeNext" : "CommonJS",
438
+ moduleResolution: isESM ? "NodeNext" : "Node",
394
439
  lib: ["ES2022"],
395
440
  outDir: "./dist",
396
441
  rootDir: "./src",
@@ -409,12 +454,13 @@ function generateTsConfig() {
409
454
  2
410
455
  );
411
456
  }
412
- function generateJsConfig() {
457
+ function generateJsConfig(options) {
458
+ const isESM = options.moduleSystem === "esm";
413
459
  return JSON.stringify(
414
460
  {
415
461
  compilerOptions: {
416
- module: "NodeNext",
417
- moduleResolution: "NodeNext",
462
+ module: isESM ? "NodeNext" : "CommonJS",
463
+ moduleResolution: isESM ? "NodeNext" : "Node",
418
464
  target: "ES2022",
419
465
  checkJs: true
420
466
  },
@@ -425,12 +471,13 @@ function generateJsConfig() {
425
471
  2
426
472
  );
427
473
  }
428
- function generateTsupConfig() {
474
+ function generateTsupConfig(options) {
475
+ const isESM = options.moduleSystem === "esm";
429
476
  return `import { defineConfig } from 'tsup';
430
477
 
431
478
  export default defineConfig({
432
479
  entry: ['src/index.ts'],
433
- format: ['esm'],
480
+ format: ['${isESM ? "esm" : "cjs"}'],
434
481
  dts: true,
435
482
  clean: true,
436
483
  sourcemap: true,
@@ -439,7 +486,7 @@ export default defineConfig({
439
486
  `;
440
487
  }
441
488
  function generateEnvExample(options) {
442
- let env = `# Server
489
+ let env2 = `# Server
443
490
  NODE_ENV=development
444
491
  PORT=3000
445
492
  HOST=0.0.0.0
@@ -457,31 +504,31 @@ RATE_LIMIT_MAX=100
457
504
  LOG_LEVEL=info
458
505
  `;
459
506
  if (options.database === "postgresql") {
460
- env += `
507
+ env2 += `
461
508
  # Database (PostgreSQL)
462
509
  DATABASE_PROVIDER=postgresql
463
510
  DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
464
511
  `;
465
512
  } else if (options.database === "mysql") {
466
- env += `
513
+ env2 += `
467
514
  # Database (MySQL)
468
515
  DATABASE_PROVIDER=mysql
469
516
  DATABASE_URL="mysql://user:password@localhost:3306/mydb"
470
517
  `;
471
518
  } else if (options.database === "sqlite") {
472
- env += `
519
+ env2 += `
473
520
  # Database (SQLite)
474
521
  DATABASE_PROVIDER=sqlite
475
522
  DATABASE_URL="file:./dev.db"
476
523
  `;
477
524
  } else if (options.database === "mongodb") {
478
- env += `
525
+ env2 += `
479
526
  # Database (MongoDB)
480
527
  MONGODB_URI="mongodb://localhost:27017/mydb"
481
528
  `;
482
529
  }
483
530
  if (options.features.includes("email")) {
484
- env += `
531
+ env2 += `
485
532
  # Email
486
533
  SMTP_HOST=smtp.example.com
487
534
  SMTP_PORT=587
@@ -491,12 +538,12 @@ SMTP_FROM="App <noreply@example.com>"
491
538
  `;
492
539
  }
493
540
  if (options.features.includes("redis")) {
494
- env += `
541
+ env2 += `
495
542
  # Redis
496
543
  REDIS_URL=redis://localhost:6379
497
544
  `;
498
545
  }
499
- return env;
546
+ return env2;
500
547
  }
501
548
  function generateGitignore() {
502
549
  return `node_modules/
@@ -625,7 +672,12 @@ model User {
625
672
  }
626
673
  function generateEntryFile(options) {
627
674
  const isTS = options.language === "typescript";
628
- return `${isTS ? "import 'dotenv/config';\nimport { createServer } from './core/server.js';\nimport { logger } from './core/logger.js';" : "require('dotenv').config();\nconst { createServer } = require('./core/server.js');\nconst { logger } = require('./core/logger.js');"}
675
+ const isESM = options.moduleSystem === "esm";
676
+ const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
677
+ if (isESM || isTS) {
678
+ return `import 'dotenv/config';
679
+ import { createServer } from './core/server.${fileExt}';
680
+ import { logger } from './core/logger.${fileExt}';
629
681
 
630
682
  async function main()${isTS ? ": Promise<void>" : ""} {
631
683
  const server = createServer();
@@ -640,16 +692,31 @@ async function main()${isTS ? ": Promise<void>" : ""} {
640
692
 
641
693
  main();
642
694
  `;
695
+ } else {
696
+ return `require('dotenv').config();
697
+ const { createServer } = require('./core/server.cjs');
698
+ const { logger } = require('./core/logger.cjs');
699
+
700
+ async function main() {
701
+ const server = createServer();
702
+
703
+ try {
704
+ await server.start();
705
+ } catch (error) {
706
+ logger.error({ err: error }, 'Failed to start server');
707
+ process.exit(1);
708
+ }
709
+ }
710
+
711
+ main();
712
+ `;
713
+ }
643
714
  }
644
715
  function generateServerFile(options) {
645
716
  const isTS = options.language === "typescript";
646
- return `${isTS ? `import Fastify from 'fastify';
647
- import type { FastifyInstance } from 'fastify';
648
- import { logger } from './logger.js';` : `const Fastify = require('fastify');
649
- const { logger } = require('./logger.js');`}
650
-
651
- ${isTS ? "export function createServer(): { instance: FastifyInstance; start: () => Promise<void> }" : "function createServer()"} {
652
- const app = Fastify({ logger });
717
+ const isESM = options.moduleSystem === "esm";
718
+ const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
719
+ const serverBody = ` const app = Fastify({ logger });
653
720
 
654
721
  // Health check
655
722
  app.get('/health', async () => ({
@@ -676,33 +743,58 @@ ${isTS ? "export function createServer(): { instance: FastifyInstance; start: ()
676
743
  logger.info(\`Server listening on \${host}:\${port}\`);
677
744
  },
678
745
  };
679
- }
746
+ }`;
747
+ if (isESM || isTS) {
748
+ return `import Fastify from 'fastify';
749
+ ${isTS ? "import type { FastifyInstance } from 'fastify';" : ""}
750
+ import { logger } from './logger.${fileExt}';
751
+
752
+ ${isTS ? "export function createServer(): { instance: FastifyInstance; start: () => Promise<void> }" : "export function createServer()"} {
753
+ ${serverBody}
754
+ `;
755
+ } else {
756
+ return `const Fastify = require('fastify');
757
+ const { logger } = require('./logger.cjs');
758
+
759
+ function createServer() {
760
+ ${serverBody}
680
761
 
681
- ${isTS ? "" : "module.exports = { createServer };"}
762
+ module.exports = { createServer };
682
763
  `;
764
+ }
683
765
  }
684
766
  function generateLoggerFile(options) {
685
767
  const isTS = options.language === "typescript";
686
- return `${isTS ? "import pino from 'pino';\nimport type { Logger } from 'pino';" : "const pino = require('pino');"}
687
-
688
- ${isTS ? "export const logger: Logger" : "const logger"} = pino({
768
+ const isESM = options.moduleSystem === "esm";
769
+ const loggerBody = `pino({
689
770
  level: process.env.LOG_LEVEL || 'info',
690
771
  transport: process.env.NODE_ENV !== 'production' ? {
691
772
  target: 'pino-pretty',
692
773
  options: { colorize: true },
693
774
  } : undefined,
694
- });
775
+ })`;
776
+ if (isESM || isTS) {
777
+ return `import pino from 'pino';
778
+ ${isTS ? "import type { Logger } from 'pino';" : ""}
779
+
780
+ export const logger${isTS ? ": Logger" : ""} = ${loggerBody};
781
+ `;
782
+ } else {
783
+ return `const pino = require('pino');
784
+
785
+ const logger = ${loggerBody};
695
786
 
696
- ${isTS ? "" : "module.exports = { logger };"}
787
+ module.exports = { logger };
697
788
  `;
789
+ }
698
790
  }
699
791
  function generateMongooseConnection(options) {
700
792
  const isTS = options.language === "typescript";
701
- return `${isTS ? "import mongoose from 'mongoose';\nimport { logger } from '../core/logger.js';" : "const mongoose = require('mongoose');\nconst { logger } = require('../core/logger.js');"}
702
-
703
- const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb';
793
+ const isESM = options.moduleSystem === "esm";
794
+ const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
795
+ const connectionBody = `const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb';
704
796
 
705
- ${isTS ? "export async function connectDatabase(): Promise<typeof mongoose>" : "async function connectDatabase()"} {
797
+ async function connectDatabase()${isTS ? ": Promise<typeof mongoose>" : ""} {
706
798
  try {
707
799
  const conn = await mongoose.connect(MONGODB_URI);
708
800
  logger.info(\`MongoDB connected: \${conn.connection.host}\`);
@@ -713,35 +805,36 @@ ${isTS ? "export async function connectDatabase(): Promise<typeof mongoose>" : "
713
805
  }
714
806
  }
715
807
 
716
- ${isTS ? "export async function disconnectDatabase(): Promise<void>" : "async function disconnectDatabase()"} {
808
+ async function disconnectDatabase()${isTS ? ": Promise<void>" : ""} {
717
809
  try {
718
810
  await mongoose.disconnect();
719
811
  logger.info('MongoDB disconnected');
720
812
  } catch (error) {
721
813
  logger.error({ err: error }, 'MongoDB disconnect failed');
722
814
  }
723
- }
815
+ }`;
816
+ if (isESM || isTS) {
817
+ return `import mongoose from 'mongoose';
818
+ import { logger } from '../core/logger.${fileExt}';
819
+
820
+ ${connectionBody}
821
+
822
+ export { connectDatabase, disconnectDatabase, mongoose };
823
+ `;
824
+ } else {
825
+ return `const mongoose = require('mongoose');
826
+ const { logger } = require('../core/logger.cjs');
724
827
 
725
- ${isTS ? "export { mongoose };" : "module.exports = { connectDatabase, disconnectDatabase, mongoose };"}
828
+ ${connectionBody}
829
+
830
+ module.exports = { connectDatabase, disconnectDatabase, mongoose };
726
831
  `;
832
+ }
727
833
  }
728
834
  function generateMongooseUserModel(options) {
729
835
  const isTS = options.language === "typescript";
730
- return `${isTS ? "import mongoose, { Schema, Document } from 'mongoose';\nimport bcrypt from 'bcryptjs';" : "const mongoose = require('mongoose');\nconst bcrypt = require('bcryptjs');\nconst { Schema } = mongoose;"}
731
-
732
- ${isTS ? `export interface IUser extends Document {
733
- email: string;
734
- password: string;
735
- name?: string;
736
- role: 'user' | 'admin';
737
- status: 'active' | 'inactive' | 'suspended';
738
- emailVerified: boolean;
739
- createdAt: Date;
740
- updatedAt: Date;
741
- comparePassword(candidatePassword: string): Promise<boolean>;
742
- }` : ""}
743
-
744
- const userSchema = new Schema${isTS ? "<IUser>" : ""}({
836
+ const isESM = options.moduleSystem === "esm";
837
+ const schemaBody = `const userSchema = new Schema${isTS ? "<IUser>" : ""}({
745
838
  email: {
746
839
  type: String,
747
840
  required: true,
@@ -792,9 +885,237 @@ userSchema.pre('save', async function(next) {
792
885
  // Compare password method
793
886
  userSchema.methods.comparePassword = async function(candidatePassword${isTS ? ": string" : ""})${isTS ? ": Promise<boolean>" : ""} {
794
887
  return bcrypt.compare(candidatePassword, this.password);
795
- };
888
+ };`;
889
+ const tsInterface = isTS ? `
890
+ export interface IUser extends Document {
891
+ email: string;
892
+ password: string;
893
+ name?: string;
894
+ role: 'user' | 'admin';
895
+ status: 'active' | 'inactive' | 'suspended';
896
+ emailVerified: boolean;
897
+ createdAt: Date;
898
+ updatedAt: Date;
899
+ comparePassword(candidatePassword: string): Promise<boolean>;
900
+ }
901
+ ` : "";
902
+ if (isESM || isTS) {
903
+ return `import mongoose${isTS ? ", { Schema, Document }" : ""} from 'mongoose';
904
+ import bcrypt from 'bcryptjs';
905
+ ${!isTS ? "const { Schema } = mongoose;" : ""}
906
+ ${tsInterface}
907
+ ${schemaBody}
908
+
909
+ export const User = mongoose.model${isTS ? "<IUser>" : ""}('User', userSchema);
910
+ `;
911
+ } else {
912
+ return `const mongoose = require('mongoose');
913
+ const bcrypt = require('bcryptjs');
914
+ const { Schema } = mongoose;
915
+
916
+ ${schemaBody}
917
+
918
+ const User = mongoose.model('User', userSchema);
919
+
920
+ module.exports = { User };
921
+ `;
922
+ }
923
+ }
924
+ function generateConfigFile(options) {
925
+ const isTS = options.language === "typescript";
926
+ const isESM = options.moduleSystem === "esm";
927
+ const configBody = `{
928
+ env: process.env.NODE_ENV || 'development',
929
+ port: parseInt(process.env.PORT || '3000', 10),
930
+ host: process.env.HOST || '0.0.0.0',
931
+
932
+ jwt: {
933
+ secret: process.env.JWT_SECRET || 'your-secret-key',
934
+ accessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN || '15m',
935
+ refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
936
+ },
937
+
938
+ cors: {
939
+ origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
940
+ },
941
+
942
+ rateLimit: {
943
+ max: parseInt(process.env.RATE_LIMIT_MAX || '100', 10),
944
+ },
945
+
946
+ log: {
947
+ level: process.env.LOG_LEVEL || 'info',
948
+ },
949
+ }${isTS ? " as const" : ""}`;
950
+ if (isESM || isTS) {
951
+ return `import 'dotenv/config';
952
+
953
+ export const config = ${configBody};
954
+ `;
955
+ } else {
956
+ return `require('dotenv').config();
957
+
958
+ const config = ${configBody};
959
+
960
+ module.exports = { config };
961
+ `;
962
+ }
963
+ }
964
+ function generateMiddlewareFile(options) {
965
+ const isTS = options.language === "typescript";
966
+ const isESM = options.moduleSystem === "esm";
967
+ const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
968
+ const middlewareBody = `/**
969
+ * Error handler middleware
970
+ */
971
+ function errorHandler(error${isTS ? ": Error" : ""}, request${isTS ? ": FastifyRequest" : ""}, reply${isTS ? ": FastifyReply" : ""})${isTS ? ": void" : ""} {
972
+ logger.error({ err: error, url: request.url, method: request.method }, 'Request error');
973
+
974
+ const statusCode = (error${isTS ? " as any" : ""}).statusCode || 500;
975
+ const message = statusCode === 500 ? 'Internal Server Error' : error.message;
976
+
977
+ reply.status(statusCode).send({
978
+ success: false,
979
+ error: message,
980
+ ...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
981
+ });
982
+ }
983
+
984
+ /**
985
+ * Request logging middleware
986
+ */
987
+ function requestLogger(request${isTS ? ": FastifyRequest" : ""}, reply${isTS ? ": FastifyReply" : ""}, done${isTS ? ": () => void" : ""})${isTS ? ": void" : ""} {
988
+ logger.info({ url: request.url, method: request.method, ip: request.ip }, 'Incoming request');
989
+ done();
990
+ }`;
991
+ if (isESM || isTS) {
992
+ return `${isTS ? "import type { FastifyRequest, FastifyReply } from 'fastify';" : ""}
993
+ import { logger } from '../core/logger.${fileExt}';
994
+
995
+ ${middlewareBody}
996
+
997
+ export { errorHandler, requestLogger };
998
+ `;
999
+ } else {
1000
+ return `const { logger } = require('../core/logger.cjs');
1001
+
1002
+ ${middlewareBody}
1003
+
1004
+ module.exports = { errorHandler, requestLogger };
1005
+ `;
1006
+ }
1007
+ }
1008
+ function generateUtilsFile(options) {
1009
+ const isTS = options.language === "typescript";
1010
+ const isESM = options.moduleSystem === "esm";
1011
+ const utilsBody = `/**
1012
+ * Standard API response helper
1013
+ */
1014
+ function apiResponse${isTS ? "<T>" : ""}(data${isTS ? ": T" : ""}, message = "Success")${isTS ? ": { success: boolean; message: string; data: T }" : ""} {
1015
+ return {
1016
+ success: true,
1017
+ message,
1018
+ data,
1019
+ };
1020
+ }
1021
+
1022
+ /**
1023
+ * Error response helper
1024
+ */
1025
+ function errorResponse(message${isTS ? ": string" : ""}, code${isTS ? "?: string" : ""})${isTS ? ": { success: boolean; error: string; code?: string }" : ""} {
1026
+ return {
1027
+ success: false,
1028
+ error: message,
1029
+ ...(code && { code }),
1030
+ };
1031
+ }
1032
+
1033
+ /**
1034
+ * Pagination helper
1035
+ */
1036
+ function paginate${isTS ? "<T>" : ""}(data${isTS ? ": T[]" : ""}, page${isTS ? ": number" : ""}, limit${isTS ? ": number" : ""}, total${isTS ? ": number" : ""})${isTS ? ": PaginationResult<T>" : ""} {
1037
+ const totalPages = Math.ceil(total / limit);
1038
+
1039
+ return {
1040
+ data,
1041
+ pagination: {
1042
+ page,
1043
+ limit,
1044
+ total,
1045
+ totalPages,
1046
+ hasNextPage: page < totalPages,
1047
+ hasPrevPage: page > 1,
1048
+ },
1049
+ };
1050
+ }`;
1051
+ const tsInterface = isTS ? `
1052
+ /**
1053
+ * Pagination result type
1054
+ */
1055
+ export interface PaginationResult<T> {
1056
+ data: T[];
1057
+ pagination: {
1058
+ page: number;
1059
+ limit: number;
1060
+ total: number;
1061
+ totalPages: number;
1062
+ hasNextPage: boolean;
1063
+ hasPrevPage: boolean;
1064
+ };
1065
+ }
1066
+ ` : "";
1067
+ if (isESM || isTS) {
1068
+ return `${tsInterface}
1069
+ ${utilsBody}
1070
+
1071
+ export { apiResponse, errorResponse, paginate };
1072
+ `;
1073
+ } else {
1074
+ return `${utilsBody}
1075
+
1076
+ module.exports = { apiResponse, errorResponse, paginate };
1077
+ `;
1078
+ }
1079
+ }
1080
+ function generateTypesFile(options) {
1081
+ if (options.language !== "typescript") {
1082
+ return "// Types file - not needed for JavaScript\n";
1083
+ }
1084
+ return `/**
1085
+ * Common type definitions
1086
+ */
796
1087
 
797
- ${isTS ? "export const User = mongoose.model<IUser>('User', userSchema);" : "const User = mongoose.model('User', userSchema);\nmodule.exports = { User };"}
1088
+ export interface ApiResponse<T = unknown> {
1089
+ success: boolean;
1090
+ message?: string;
1091
+ data?: T;
1092
+ error?: string;
1093
+ code?: string;
1094
+ }
1095
+
1096
+ export interface PaginatedResponse<T> {
1097
+ data: T[];
1098
+ pagination: {
1099
+ page: number;
1100
+ limit: number;
1101
+ total: number;
1102
+ totalPages: number;
1103
+ hasNextPage: boolean;
1104
+ hasPrevPage: boolean;
1105
+ };
1106
+ }
1107
+
1108
+ export interface RequestUser {
1109
+ id: string;
1110
+ email: string;
1111
+ role: string;
1112
+ }
1113
+
1114
+ declare module 'fastify' {
1115
+ interface FastifyRequest {
1116
+ user?: RequestUser;
1117
+ }
1118
+ }
798
1119
  `;
799
1120
  }
800
1121
 
@@ -1963,12 +2284,12 @@ var EnvManager = class {
1963
2284
  async addVariables(sections) {
1964
2285
  const added = [];
1965
2286
  const skipped = [];
1966
- let created = false;
2287
+ let created2 = false;
1967
2288
  let envContent = "";
1968
2289
  if ((0, import_fs.existsSync)(this.envPath)) {
1969
2290
  envContent = await fs3.readFile(this.envPath, "utf-8");
1970
2291
  } else {
1971
- created = true;
2292
+ created2 = true;
1972
2293
  }
1973
2294
  const existingKeys = this.parseExistingKeys(envContent);
1974
2295
  let newContent = envContent;
@@ -1999,7 +2320,7 @@ var EnvManager = class {
1999
2320
  if ((0, import_fs.existsSync)(this.envExamplePath)) {
2000
2321
  await this.updateEnvExample(sections);
2001
2322
  }
2002
- return { added, skipped, created };
2323
+ return { added, skipped, created: created2 };
2003
2324
  }
2004
2325
  /**
2005
2326
  * Update .env.example file
@@ -3582,7 +3903,7 @@ async function findServercraftModules() {
3582
3903
  }
3583
3904
  async function generateModuleFiles(moduleName, moduleDir) {
3584
3905
  const moduleNameMap = {
3585
- "users": "user",
3906
+ users: "user",
3586
3907
  "rate-limit": "rate-limit",
3587
3908
  "feature-flag": "feature-flag",
3588
3909
  "api-versioning": "api-versioning",
@@ -3851,12 +4172,1782 @@ dbCommand.command("status").description("Show migration status").action(async ()
3851
4172
  }
3852
4173
  });
3853
4174
 
4175
+ // src/cli/commands/docs.ts
4176
+ var import_commander5 = require("commander");
4177
+ var import_path6 = __toESM(require("path"), 1);
4178
+ var import_promises4 = __toESM(require("fs/promises"), 1);
4179
+ var import_ora6 = __toESM(require("ora"), 1);
4180
+ var import_chalk6 = __toESM(require("chalk"), 1);
4181
+
4182
+ // src/cli/utils/docs-generator.ts
4183
+ var import_promises3 = __toESM(require("fs/promises"), 1);
4184
+ var import_path5 = __toESM(require("path"), 1);
4185
+ var import_ora5 = __toESM(require("ora"), 1);
4186
+
4187
+ // src/core/server.ts
4188
+ var import_fastify = __toESM(require("fastify"), 1);
4189
+
4190
+ // src/core/logger.ts
4191
+ var import_pino = __toESM(require("pino"), 1);
4192
+ var defaultConfig = {
4193
+ level: process.env.LOG_LEVEL || "info",
4194
+ pretty: process.env.NODE_ENV !== "production",
4195
+ name: "servcraft"
4196
+ };
4197
+ function createLogger(config2 = {}) {
4198
+ const mergedConfig = { ...defaultConfig, ...config2 };
4199
+ const transport = mergedConfig.pretty ? {
4200
+ target: "pino-pretty",
4201
+ options: {
4202
+ colorize: true,
4203
+ translateTime: "SYS:standard",
4204
+ ignore: "pid,hostname"
4205
+ }
4206
+ } : void 0;
4207
+ return (0, import_pino.default)({
4208
+ name: mergedConfig.name,
4209
+ level: mergedConfig.level,
4210
+ transport,
4211
+ formatters: {
4212
+ level: (label) => ({ level: label })
4213
+ },
4214
+ timestamp: import_pino.default.stdTimeFunctions.isoTime
4215
+ });
4216
+ }
4217
+ var logger = createLogger();
4218
+
4219
+ // src/core/server.ts
4220
+ var defaultConfig2 = {
4221
+ port: parseInt(process.env.PORT || "3000", 10),
4222
+ host: process.env.HOST || "0.0.0.0",
4223
+ trustProxy: true,
4224
+ bodyLimit: 1048576,
4225
+ // 1MB
4226
+ requestTimeout: 3e4
4227
+ // 30s
4228
+ };
4229
+ var Server = class {
4230
+ app;
4231
+ config;
4232
+ logger;
4233
+ isShuttingDown = false;
4234
+ constructor(config2 = {}) {
4235
+ this.config = { ...defaultConfig2, ...config2 };
4236
+ this.logger = this.config.logger || logger;
4237
+ const fastifyOptions = {
4238
+ logger: this.logger,
4239
+ trustProxy: this.config.trustProxy,
4240
+ bodyLimit: this.config.bodyLimit,
4241
+ requestTimeout: this.config.requestTimeout
4242
+ };
4243
+ this.app = (0, import_fastify.default)(fastifyOptions);
4244
+ this.setupHealthCheck();
4245
+ this.setupGracefulShutdown();
4246
+ }
4247
+ get instance() {
4248
+ return this.app;
4249
+ }
4250
+ setupHealthCheck() {
4251
+ this.app.get("/health", async (_request, reply) => {
4252
+ const healthcheck = {
4253
+ status: "ok",
4254
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4255
+ uptime: process.uptime(),
4256
+ memory: process.memoryUsage(),
4257
+ version: process.env.npm_package_version || "0.1.0"
4258
+ };
4259
+ return reply.status(200).send(healthcheck);
4260
+ });
4261
+ this.app.get("/ready", async (_request, reply) => {
4262
+ if (this.isShuttingDown) {
4263
+ return reply.status(503).send({ status: "shutting_down" });
4264
+ }
4265
+ return reply.status(200).send({ status: "ready" });
4266
+ });
4267
+ }
4268
+ setupGracefulShutdown() {
4269
+ const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
4270
+ signals.forEach((signal) => {
4271
+ process.on(signal, async () => {
4272
+ this.logger.info(`Received ${signal}, starting graceful shutdown...`);
4273
+ await this.shutdown();
4274
+ });
4275
+ });
4276
+ process.on("uncaughtException", async (error2) => {
4277
+ this.logger.error({ err: error2 }, "Uncaught exception");
4278
+ await this.shutdown(1);
4279
+ });
4280
+ process.on("unhandledRejection", async (reason) => {
4281
+ this.logger.error({ err: reason }, "Unhandled rejection");
4282
+ await this.shutdown(1);
4283
+ });
4284
+ }
4285
+ async shutdown(exitCode = 0) {
4286
+ if (this.isShuttingDown) {
4287
+ return;
4288
+ }
4289
+ this.isShuttingDown = true;
4290
+ this.logger.info("Graceful shutdown initiated...");
4291
+ const shutdownTimeout = setTimeout(() => {
4292
+ this.logger.error("Graceful shutdown timeout, forcing exit");
4293
+ process.exit(1);
4294
+ }, 3e4);
4295
+ try {
4296
+ await this.app.close();
4297
+ this.logger.info("Server closed successfully");
4298
+ clearTimeout(shutdownTimeout);
4299
+ process.exit(exitCode);
4300
+ } catch (error2) {
4301
+ this.logger.error({ err: error2 }, "Error during shutdown");
4302
+ clearTimeout(shutdownTimeout);
4303
+ process.exit(1);
4304
+ }
4305
+ }
4306
+ async start() {
4307
+ try {
4308
+ await this.app.listen({
4309
+ port: this.config.port,
4310
+ host: this.config.host
4311
+ });
4312
+ this.logger.info(`Server listening on ${this.config.host}:${this.config.port}`);
4313
+ } catch (error2) {
4314
+ this.logger.error({ err: error2 }, "Failed to start server");
4315
+ throw error2;
4316
+ }
4317
+ }
4318
+ };
4319
+ function createServer(config2 = {}) {
4320
+ return new Server(config2);
4321
+ }
4322
+
4323
+ // src/utils/errors.ts
4324
+ var AppError = class _AppError extends Error {
4325
+ statusCode;
4326
+ isOperational;
4327
+ errors;
4328
+ constructor(message, statusCode = 500, isOperational = true, errors) {
4329
+ super(message);
4330
+ this.statusCode = statusCode;
4331
+ this.isOperational = isOperational;
4332
+ this.errors = errors;
4333
+ Object.setPrototypeOf(this, _AppError.prototype);
4334
+ Error.captureStackTrace(this, this.constructor);
4335
+ }
4336
+ };
4337
+ var NotFoundError = class _NotFoundError extends AppError {
4338
+ constructor(resource = "Resource") {
4339
+ super(`${resource} not found`, 404);
4340
+ Object.setPrototypeOf(this, _NotFoundError.prototype);
4341
+ }
4342
+ };
4343
+ var UnauthorizedError = class _UnauthorizedError extends AppError {
4344
+ constructor(message = "Unauthorized") {
4345
+ super(message, 401);
4346
+ Object.setPrototypeOf(this, _UnauthorizedError.prototype);
4347
+ }
4348
+ };
4349
+ var ForbiddenError = class _ForbiddenError extends AppError {
4350
+ constructor(message = "Forbidden") {
4351
+ super(message, 403);
4352
+ Object.setPrototypeOf(this, _ForbiddenError.prototype);
4353
+ }
4354
+ };
4355
+ var BadRequestError = class _BadRequestError extends AppError {
4356
+ constructor(message = "Bad request", errors) {
4357
+ super(message, 400, true, errors);
4358
+ Object.setPrototypeOf(this, _BadRequestError.prototype);
4359
+ }
4360
+ };
4361
+ var ConflictError = class _ConflictError extends AppError {
4362
+ constructor(message = "Resource already exists") {
4363
+ super(message, 409);
4364
+ Object.setPrototypeOf(this, _ConflictError.prototype);
4365
+ }
4366
+ };
4367
+ var ValidationError = class _ValidationError extends AppError {
4368
+ constructor(errors) {
4369
+ super("Validation failed", 422, true, errors);
4370
+ Object.setPrototypeOf(this, _ValidationError.prototype);
4371
+ }
4372
+ };
4373
+ function isAppError(error2) {
4374
+ return error2 instanceof AppError;
4375
+ }
4376
+
4377
+ // src/config/env.ts
4378
+ var import_zod = require("zod");
4379
+ var import_dotenv = __toESM(require("dotenv"), 1);
4380
+ import_dotenv.default.config();
4381
+ var envSchema = import_zod.z.object({
4382
+ // Server
4383
+ NODE_ENV: import_zod.z.enum(["development", "staging", "production", "test"]).default("development"),
4384
+ PORT: import_zod.z.string().transform(Number).default("3000"),
4385
+ HOST: import_zod.z.string().default("0.0.0.0"),
4386
+ // Database
4387
+ DATABASE_URL: import_zod.z.string().optional(),
4388
+ // JWT
4389
+ JWT_SECRET: import_zod.z.string().min(32).optional(),
4390
+ JWT_ACCESS_EXPIRES_IN: import_zod.z.string().default("15m"),
4391
+ JWT_REFRESH_EXPIRES_IN: import_zod.z.string().default("7d"),
4392
+ // Security
4393
+ CORS_ORIGIN: import_zod.z.string().default("*"),
4394
+ RATE_LIMIT_MAX: import_zod.z.string().transform(Number).default("100"),
4395
+ RATE_LIMIT_WINDOW_MS: import_zod.z.string().transform(Number).default("60000"),
4396
+ // Email
4397
+ SMTP_HOST: import_zod.z.string().optional(),
4398
+ SMTP_PORT: import_zod.z.string().transform(Number).optional(),
4399
+ SMTP_USER: import_zod.z.string().optional(),
4400
+ SMTP_PASS: import_zod.z.string().optional(),
4401
+ SMTP_FROM: import_zod.z.string().optional(),
4402
+ // Redis (optional)
4403
+ REDIS_URL: import_zod.z.string().optional(),
4404
+ // Logging
4405
+ LOG_LEVEL: import_zod.z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info")
4406
+ });
4407
+ function validateEnv() {
4408
+ const parsed = envSchema.safeParse(process.env);
4409
+ if (!parsed.success) {
4410
+ logger.error({ errors: parsed.error.flatten().fieldErrors }, "Invalid environment variables");
4411
+ throw new Error("Invalid environment variables");
4412
+ }
4413
+ return parsed.data;
4414
+ }
4415
+ var env = validateEnv();
4416
+ function isProduction() {
4417
+ return env.NODE_ENV === "production";
4418
+ }
4419
+
4420
+ // src/config/index.ts
4421
+ function parseCorsOrigin(origin) {
4422
+ if (origin === "*") return "*";
4423
+ if (origin.includes(",")) {
4424
+ return origin.split(",").map((o) => o.trim());
4425
+ }
4426
+ return origin;
4427
+ }
4428
+ function createConfig() {
4429
+ return {
4430
+ env,
4431
+ server: {
4432
+ port: env.PORT,
4433
+ host: env.HOST
4434
+ },
4435
+ jwt: {
4436
+ secret: env.JWT_SECRET || "change-me-in-production-please-32chars",
4437
+ accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
4438
+ refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN
4439
+ },
4440
+ security: {
4441
+ corsOrigin: parseCorsOrigin(env.CORS_ORIGIN),
4442
+ rateLimit: {
4443
+ max: env.RATE_LIMIT_MAX,
4444
+ windowMs: env.RATE_LIMIT_WINDOW_MS
4445
+ }
4446
+ },
4447
+ email: {
4448
+ host: env.SMTP_HOST,
4449
+ port: env.SMTP_PORT,
4450
+ user: env.SMTP_USER,
4451
+ pass: env.SMTP_PASS,
4452
+ from: env.SMTP_FROM
4453
+ },
4454
+ database: {
4455
+ url: env.DATABASE_URL
4456
+ },
4457
+ redis: {
4458
+ url: env.REDIS_URL
4459
+ }
4460
+ };
4461
+ }
4462
+ var config = createConfig();
4463
+
4464
+ // src/middleware/error-handler.ts
4465
+ function registerErrorHandler(app) {
4466
+ app.setErrorHandler(
4467
+ (error2, request, reply) => {
4468
+ logger.error(
4469
+ {
4470
+ err: error2,
4471
+ requestId: request.id,
4472
+ method: request.method,
4473
+ url: request.url
4474
+ },
4475
+ "Request error"
4476
+ );
4477
+ if (isAppError(error2)) {
4478
+ return reply.status(error2.statusCode).send({
4479
+ success: false,
4480
+ message: error2.message,
4481
+ errors: error2.errors,
4482
+ ...isProduction() ? {} : { stack: error2.stack }
4483
+ });
4484
+ }
4485
+ if ("validation" in error2 && error2.validation) {
4486
+ const errors = {};
4487
+ for (const err of error2.validation) {
4488
+ const field = err.instancePath?.replace("/", "") || "body";
4489
+ if (!errors[field]) {
4490
+ errors[field] = [];
4491
+ }
4492
+ errors[field].push(err.message || "Invalid value");
4493
+ }
4494
+ return reply.status(400).send({
4495
+ success: false,
4496
+ message: "Validation failed",
4497
+ errors
4498
+ });
4499
+ }
4500
+ if ("statusCode" in error2 && typeof error2.statusCode === "number") {
4501
+ return reply.status(error2.statusCode).send({
4502
+ success: false,
4503
+ message: error2.message,
4504
+ ...isProduction() ? {} : { stack: error2.stack }
4505
+ });
4506
+ }
4507
+ return reply.status(500).send({
4508
+ success: false,
4509
+ message: isProduction() ? "Internal server error" : error2.message,
4510
+ ...isProduction() ? {} : { stack: error2.stack }
4511
+ });
4512
+ }
4513
+ );
4514
+ app.setNotFoundHandler((request, reply) => {
4515
+ return reply.status(404).send({
4516
+ success: false,
4517
+ message: `Route ${request.method} ${request.url} not found`
4518
+ });
4519
+ });
4520
+ }
4521
+
4522
+ // src/middleware/security.ts
4523
+ var import_helmet = __toESM(require("@fastify/helmet"), 1);
4524
+ var import_cors = __toESM(require("@fastify/cors"), 1);
4525
+ var import_rate_limit = __toESM(require("@fastify/rate-limit"), 1);
4526
+ var defaultOptions = {
4527
+ helmet: true,
4528
+ cors: true,
4529
+ rateLimit: true
4530
+ };
4531
+ async function registerSecurity(app, options = {}) {
4532
+ const opts = { ...defaultOptions, ...options };
4533
+ if (opts.helmet) {
4534
+ await app.register(import_helmet.default, {
4535
+ contentSecurityPolicy: {
4536
+ directives: {
4537
+ defaultSrc: ["'self'"],
4538
+ styleSrc: ["'self'", "'unsafe-inline'"],
4539
+ scriptSrc: ["'self'"],
4540
+ imgSrc: ["'self'", "data:", "https:"]
4541
+ }
4542
+ },
4543
+ crossOriginEmbedderPolicy: false
4544
+ });
4545
+ logger.debug("Helmet security headers enabled");
4546
+ }
4547
+ if (opts.cors) {
4548
+ await app.register(import_cors.default, {
4549
+ origin: config.security.corsOrigin,
4550
+ credentials: true,
4551
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
4552
+ allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
4553
+ exposedHeaders: ["X-Total-Count", "X-Page", "X-Limit"],
4554
+ maxAge: 86400
4555
+ // 24 hours
4556
+ });
4557
+ logger.debug({ origin: config.security.corsOrigin }, "CORS enabled");
4558
+ }
4559
+ if (opts.rateLimit) {
4560
+ await app.register(import_rate_limit.default, {
4561
+ max: config.security.rateLimit.max,
4562
+ timeWindow: config.security.rateLimit.windowMs,
4563
+ errorResponseBuilder: (_request, context) => ({
4564
+ success: false,
4565
+ message: "Too many requests, please try again later",
4566
+ retryAfter: context.after
4567
+ }),
4568
+ keyGenerator: (request) => {
4569
+ return request.headers["x-forwarded-for"]?.toString().split(",")[0] || request.ip || "unknown";
4570
+ }
4571
+ });
4572
+ logger.debug(
4573
+ {
4574
+ max: config.security.rateLimit.max,
4575
+ windowMs: config.security.rateLimit.windowMs
4576
+ },
4577
+ "Rate limiting enabled"
4578
+ );
4579
+ }
4580
+ }
4581
+
4582
+ // src/modules/swagger/swagger.service.ts
4583
+ var import_swagger = __toESM(require("@fastify/swagger"), 1);
4584
+ var import_swagger_ui = __toESM(require("@fastify/swagger-ui"), 1);
4585
+ var defaultConfig3 = {
4586
+ title: "Servcraft API",
4587
+ description: "API documentation generated by Servcraft",
4588
+ version: "1.0.0",
4589
+ tags: [
4590
+ { name: "Auth", description: "Authentication endpoints" },
4591
+ { name: "Users", description: "User management endpoints" },
4592
+ { name: "Health", description: "Health check endpoints" }
4593
+ ]
4594
+ };
4595
+ async function registerSwagger(app, customConfig) {
4596
+ const swaggerConfig = { ...defaultConfig3, ...customConfig };
4597
+ await app.register(import_swagger.default, {
4598
+ openapi: {
4599
+ openapi: "3.0.3",
4600
+ info: {
4601
+ title: swaggerConfig.title,
4602
+ description: swaggerConfig.description,
4603
+ version: swaggerConfig.version,
4604
+ contact: swaggerConfig.contact,
4605
+ license: swaggerConfig.license
4606
+ },
4607
+ servers: swaggerConfig.servers || [
4608
+ {
4609
+ url: `http://localhost:${config.server.port}`,
4610
+ description: "Development server"
4611
+ }
4612
+ ],
4613
+ tags: swaggerConfig.tags,
4614
+ components: {
4615
+ securitySchemes: {
4616
+ bearerAuth: {
4617
+ type: "http",
4618
+ scheme: "bearer",
4619
+ bearerFormat: "JWT",
4620
+ description: "Enter your JWT token"
4621
+ }
4622
+ }
4623
+ }
4624
+ }
4625
+ });
4626
+ await app.register(import_swagger_ui.default, {
4627
+ routePrefix: "/docs",
4628
+ uiConfig: {
4629
+ docExpansion: "list",
4630
+ deepLinking: true,
4631
+ displayRequestDuration: true,
4632
+ filter: true,
4633
+ showExtensions: true,
4634
+ showCommonExtensions: true
4635
+ },
4636
+ staticCSP: true,
4637
+ transformStaticCSP: (header) => header
4638
+ });
4639
+ logger.info("Swagger documentation registered at /docs");
4640
+ }
4641
+
4642
+ // src/modules/auth/index.ts
4643
+ var import_jwt = __toESM(require("@fastify/jwt"), 1);
4644
+ var import_cookie = __toESM(require("@fastify/cookie"), 1);
4645
+
4646
+ // src/modules/auth/auth.service.ts
4647
+ var import_bcryptjs = __toESM(require("bcryptjs"), 1);
4648
+ var import_ioredis = require("ioredis");
4649
+ var AuthService = class {
4650
+ app;
4651
+ SALT_ROUNDS = 12;
4652
+ redis = null;
4653
+ BLACKLIST_PREFIX = "auth:blacklist:";
4654
+ BLACKLIST_TTL = 7 * 24 * 60 * 60;
4655
+ // 7 days in seconds
4656
+ constructor(app, redisUrl) {
4657
+ this.app = app;
4658
+ if (redisUrl || process.env.REDIS_URL) {
4659
+ try {
4660
+ this.redis = new import_ioredis.Redis(redisUrl || process.env.REDIS_URL || "redis://localhost:6379");
4661
+ this.redis.on("connect", () => {
4662
+ logger.info("Auth service connected to Redis for token blacklist");
4663
+ });
4664
+ this.redis.on("error", (error2) => {
4665
+ logger.error({ err: error2 }, "Redis connection error in Auth service");
4666
+ });
4667
+ } catch (error2) {
4668
+ logger.warn({ err: error2 }, "Failed to connect to Redis, using in-memory blacklist");
4669
+ this.redis = null;
4670
+ }
4671
+ } else {
4672
+ logger.warn(
4673
+ "No REDIS_URL provided, using in-memory token blacklist (not recommended for production)"
4674
+ );
4675
+ }
4676
+ }
4677
+ async hashPassword(password) {
4678
+ return import_bcryptjs.default.hash(password, this.SALT_ROUNDS);
4679
+ }
4680
+ async verifyPassword(password, hash) {
4681
+ return import_bcryptjs.default.compare(password, hash);
4682
+ }
4683
+ generateTokenPair(user) {
4684
+ const accessPayload = {
4685
+ sub: user.id,
4686
+ email: user.email,
4687
+ role: user.role,
4688
+ type: "access"
4689
+ };
4690
+ const refreshPayload = {
4691
+ sub: user.id,
4692
+ email: user.email,
4693
+ role: user.role,
4694
+ type: "refresh"
4695
+ };
4696
+ const accessToken = this.app.jwt.sign(accessPayload, {
4697
+ expiresIn: config.jwt.accessExpiresIn
4698
+ });
4699
+ const refreshToken = this.app.jwt.sign(refreshPayload, {
4700
+ expiresIn: config.jwt.refreshExpiresIn
4701
+ });
4702
+ const expiresIn = this.parseExpiration(config.jwt.accessExpiresIn);
4703
+ return { accessToken, refreshToken, expiresIn };
4704
+ }
4705
+ parseExpiration(expiration) {
4706
+ const match = expiration.match(/^(\d+)([smhd])$/);
4707
+ if (!match) return 900;
4708
+ const value = parseInt(match[1] || "0", 10);
4709
+ const unit = match[2];
4710
+ switch (unit) {
4711
+ case "s":
4712
+ return value;
4713
+ case "m":
4714
+ return value * 60;
4715
+ case "h":
4716
+ return value * 3600;
4717
+ case "d":
4718
+ return value * 86400;
4719
+ default:
4720
+ return 900;
4721
+ }
4722
+ }
4723
+ async verifyAccessToken(token) {
4724
+ try {
4725
+ if (await this.isTokenBlacklisted(token)) {
4726
+ throw new UnauthorizedError("Token has been revoked");
4727
+ }
4728
+ const payload = this.app.jwt.verify(token);
4729
+ if (payload.type !== "access") {
4730
+ throw new UnauthorizedError("Invalid token type");
4731
+ }
4732
+ return payload;
4733
+ } catch (error2) {
4734
+ if (error2 instanceof UnauthorizedError) throw error2;
4735
+ logger.debug({ err: error2 }, "Token verification failed");
4736
+ throw new UnauthorizedError("Invalid or expired token");
4737
+ }
4738
+ }
4739
+ async verifyRefreshToken(token) {
4740
+ try {
4741
+ if (await this.isTokenBlacklisted(token)) {
4742
+ throw new UnauthorizedError("Token has been revoked");
4743
+ }
4744
+ const payload = this.app.jwt.verify(token);
4745
+ if (payload.type !== "refresh") {
4746
+ throw new UnauthorizedError("Invalid token type");
4747
+ }
4748
+ return payload;
4749
+ } catch (error2) {
4750
+ if (error2 instanceof UnauthorizedError) throw error2;
4751
+ logger.debug({ err: error2 }, "Refresh token verification failed");
4752
+ throw new UnauthorizedError("Invalid or expired refresh token");
4753
+ }
4754
+ }
4755
+ /**
4756
+ * Blacklist a token (JWT revocation)
4757
+ * Uses Redis if available, falls back to in-memory Set
4758
+ */
4759
+ async blacklistToken(token) {
4760
+ if (this.redis) {
4761
+ try {
4762
+ const key = `${this.BLACKLIST_PREFIX}${token}`;
4763
+ await this.redis.setex(key, this.BLACKLIST_TTL, "1");
4764
+ logger.debug("Token blacklisted in Redis");
4765
+ } catch (error2) {
4766
+ logger.error({ err: error2 }, "Failed to blacklist token in Redis");
4767
+ throw new Error("Failed to revoke token");
4768
+ }
4769
+ } else {
4770
+ logger.warn("Using in-memory blacklist - not suitable for multi-instance deployments");
4771
+ }
4772
+ }
4773
+ /**
4774
+ * Check if a token is blacklisted
4775
+ * Uses Redis if available, falls back to always returning false
4776
+ */
4777
+ async isTokenBlacklisted(token) {
4778
+ if (this.redis) {
4779
+ try {
4780
+ const key = `${this.BLACKLIST_PREFIX}${token}`;
4781
+ const result = await this.redis.exists(key);
4782
+ return result === 1;
4783
+ } catch (error2) {
4784
+ logger.error({ err: error2 }, "Failed to check token blacklist in Redis");
4785
+ return false;
4786
+ }
4787
+ }
4788
+ return false;
4789
+ }
4790
+ /**
4791
+ * Get count of blacklisted tokens (Redis only)
4792
+ */
4793
+ async getBlacklistCount() {
4794
+ if (this.redis) {
4795
+ try {
4796
+ const keys = await this.redis.keys(`${this.BLACKLIST_PREFIX}*`);
4797
+ return keys.length;
4798
+ } catch (error2) {
4799
+ logger.error({ err: error2 }, "Failed to get blacklist count from Redis");
4800
+ return 0;
4801
+ }
4802
+ }
4803
+ return 0;
4804
+ }
4805
+ /**
4806
+ * Close Redis connection
4807
+ */
4808
+ async close() {
4809
+ if (this.redis) {
4810
+ await this.redis.quit();
4811
+ logger.info("Auth service Redis connection closed");
4812
+ }
4813
+ }
4814
+ // OAuth support methods - to be implemented with user repository
4815
+ async findUserByEmail(_email) {
4816
+ return null;
4817
+ }
4818
+ async createUserFromOAuth(data) {
4819
+ const user = {
4820
+ id: `oauth_${Date.now()}`,
4821
+ email: data.email,
4822
+ role: "user"
4823
+ };
4824
+ logger.info({ email: data.email }, "Created user from OAuth");
4825
+ return user;
4826
+ }
4827
+ async generateTokensForUser(userId) {
4828
+ const user = {
4829
+ id: userId,
4830
+ email: "",
4831
+ // Would be fetched from database in production
4832
+ role: "user"
4833
+ };
4834
+ return this.generateTokenPair(user);
4835
+ }
4836
+ async verifyPasswordById(userId, _password) {
4837
+ logger.debug({ userId }, "Password verification requested");
4838
+ return false;
4839
+ }
4840
+ };
4841
+ function createAuthService(app) {
4842
+ return new AuthService(app);
4843
+ }
4844
+
4845
+ // src/modules/auth/schemas.ts
4846
+ var import_zod2 = require("zod");
4847
+ var loginSchema = import_zod2.z.object({
4848
+ email: import_zod2.z.string().email("Invalid email address"),
4849
+ password: import_zod2.z.string().min(1, "Password is required")
4850
+ });
4851
+ var registerSchema = import_zod2.z.object({
4852
+ email: import_zod2.z.string().email("Invalid email address"),
4853
+ password: import_zod2.z.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number"),
4854
+ name: import_zod2.z.string().min(2, "Name must be at least 2 characters").optional()
4855
+ });
4856
+ var refreshTokenSchema = import_zod2.z.object({
4857
+ refreshToken: import_zod2.z.string().min(1, "Refresh token is required")
4858
+ });
4859
+ var passwordResetRequestSchema = import_zod2.z.object({
4860
+ email: import_zod2.z.string().email("Invalid email address")
4861
+ });
4862
+ var passwordResetConfirmSchema = import_zod2.z.object({
4863
+ token: import_zod2.z.string().min(1, "Token is required"),
4864
+ password: import_zod2.z.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number")
4865
+ });
4866
+ var changePasswordSchema = import_zod2.z.object({
4867
+ currentPassword: import_zod2.z.string().min(1, "Current password is required"),
4868
+ newPassword: import_zod2.z.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number")
4869
+ });
4870
+
4871
+ // src/utils/response.ts
4872
+ function success2(reply, data, statusCode = 200) {
4873
+ const response = {
4874
+ success: true,
4875
+ data
4876
+ };
4877
+ return reply.status(statusCode).send(response);
4878
+ }
4879
+ function created(reply, data) {
4880
+ return success2(reply, data, 201);
4881
+ }
4882
+ function noContent(reply) {
4883
+ return reply.status(204).send();
4884
+ }
4885
+
4886
+ // src/modules/validation/validator.ts
4887
+ var import_zod3 = require("zod");
4888
+ function validateBody(schema, data) {
4889
+ const result = schema.safeParse(data);
4890
+ if (!result.success) {
4891
+ throw new ValidationError(formatZodErrors(result.error));
4892
+ }
4893
+ return result.data;
4894
+ }
4895
+ function validateQuery(schema, data) {
4896
+ const result = schema.safeParse(data);
4897
+ if (!result.success) {
4898
+ throw new ValidationError(formatZodErrors(result.error));
4899
+ }
4900
+ return result.data;
4901
+ }
4902
+ function formatZodErrors(error2) {
4903
+ const errors = {};
4904
+ for (const issue of error2.issues) {
4905
+ const path9 = issue.path.join(".") || "root";
4906
+ if (!errors[path9]) {
4907
+ errors[path9] = [];
4908
+ }
4909
+ errors[path9].push(issue.message);
4910
+ }
4911
+ return errors;
4912
+ }
4913
+ var idParamSchema = import_zod3.z.object({
4914
+ id: import_zod3.z.string().uuid("Invalid ID format")
4915
+ });
4916
+ var paginationSchema = import_zod3.z.object({
4917
+ page: import_zod3.z.string().transform(Number).optional().default("1"),
4918
+ limit: import_zod3.z.string().transform(Number).optional().default("20"),
4919
+ sortBy: import_zod3.z.string().optional(),
4920
+ sortOrder: import_zod3.z.enum(["asc", "desc"]).optional().default("asc")
4921
+ });
4922
+ var searchSchema = import_zod3.z.object({
4923
+ q: import_zod3.z.string().min(1, "Search query is required").optional(),
4924
+ search: import_zod3.z.string().min(1).optional()
4925
+ });
4926
+ var emailSchema = import_zod3.z.string().email("Invalid email address");
4927
+ var passwordSchema = import_zod3.z.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number").regex(/[^A-Za-z0-9]/, "Password must contain at least one special character");
4928
+ var urlSchema = import_zod3.z.string().url("Invalid URL format");
4929
+ var phoneSchema = import_zod3.z.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number format");
4930
+ var dateSchema = import_zod3.z.coerce.date();
4931
+ var futureDateSchema = import_zod3.z.coerce.date().refine((date) => date > /* @__PURE__ */ new Date(), "Date must be in the future");
4932
+ var pastDateSchema = import_zod3.z.coerce.date().refine((date) => date < /* @__PURE__ */ new Date(), "Date must be in the past");
4933
+
4934
+ // src/modules/auth/auth.controller.ts
4935
+ var AuthController = class {
4936
+ constructor(authService, userService) {
4937
+ this.authService = authService;
4938
+ this.userService = userService;
4939
+ }
4940
+ async register(request, reply) {
4941
+ const data = validateBody(registerSchema, request.body);
4942
+ const existingUser = await this.userService.findByEmail(data.email);
4943
+ if (existingUser) {
4944
+ throw new BadRequestError("Email already registered");
4945
+ }
4946
+ const hashedPassword = await this.authService.hashPassword(data.password);
4947
+ const user = await this.userService.create({
4948
+ email: data.email,
4949
+ password: hashedPassword,
4950
+ name: data.name
4951
+ });
4952
+ const tokens = this.authService.generateTokenPair({
4953
+ id: user.id,
4954
+ email: user.email,
4955
+ role: user.role
4956
+ });
4957
+ created(reply, {
4958
+ user: {
4959
+ id: user.id,
4960
+ email: user.email,
4961
+ name: user.name,
4962
+ role: user.role
4963
+ },
4964
+ ...tokens
4965
+ });
4966
+ }
4967
+ async login(request, reply) {
4968
+ const data = validateBody(loginSchema, request.body);
4969
+ const user = await this.userService.findByEmail(data.email);
4970
+ if (!user) {
4971
+ throw new UnauthorizedError("Invalid credentials");
4972
+ }
4973
+ if (user.status !== "active") {
4974
+ throw new UnauthorizedError("Account is not active");
4975
+ }
4976
+ const isValidPassword = await this.authService.verifyPassword(data.password, user.password);
4977
+ if (!isValidPassword) {
4978
+ throw new UnauthorizedError("Invalid credentials");
4979
+ }
4980
+ await this.userService.updateLastLogin(user.id);
4981
+ const tokens = this.authService.generateTokenPair({
4982
+ id: user.id,
4983
+ email: user.email,
4984
+ role: user.role
4985
+ });
4986
+ success2(reply, {
4987
+ user: {
4988
+ id: user.id,
4989
+ email: user.email,
4990
+ name: user.name,
4991
+ role: user.role
4992
+ },
4993
+ ...tokens
4994
+ });
4995
+ }
4996
+ async refresh(request, reply) {
4997
+ const data = validateBody(refreshTokenSchema, request.body);
4998
+ const payload = await this.authService.verifyRefreshToken(data.refreshToken);
4999
+ const user = await this.userService.findById(payload.sub);
5000
+ if (!user || user.status !== "active") {
5001
+ throw new UnauthorizedError("User not found or inactive");
5002
+ }
5003
+ await this.authService.blacklistToken(data.refreshToken);
5004
+ const tokens = this.authService.generateTokenPair({
5005
+ id: user.id,
5006
+ email: user.email,
5007
+ role: user.role
5008
+ });
5009
+ success2(reply, tokens);
5010
+ }
5011
+ async logout(request, reply) {
5012
+ const authHeader = request.headers.authorization;
5013
+ if (authHeader?.startsWith("Bearer ")) {
5014
+ const token = authHeader.substring(7);
5015
+ await this.authService.blacklistToken(token);
5016
+ }
5017
+ success2(reply, { message: "Logged out successfully" });
5018
+ }
5019
+ async me(request, reply) {
5020
+ const authRequest = request;
5021
+ const user = await this.userService.findById(authRequest.user.id);
5022
+ if (!user) {
5023
+ throw new UnauthorizedError("User not found");
5024
+ }
5025
+ success2(reply, {
5026
+ id: user.id,
5027
+ email: user.email,
5028
+ name: user.name,
5029
+ role: user.role,
5030
+ status: user.status,
5031
+ createdAt: user.createdAt
5032
+ });
5033
+ }
5034
+ async changePassword(request, reply) {
5035
+ const authRequest = request;
5036
+ const data = validateBody(changePasswordSchema, request.body);
5037
+ const user = await this.userService.findById(authRequest.user.id);
5038
+ if (!user) {
5039
+ throw new UnauthorizedError("User not found");
5040
+ }
5041
+ const isValidPassword = await this.authService.verifyPassword(
5042
+ data.currentPassword,
5043
+ user.password
5044
+ );
5045
+ if (!isValidPassword) {
5046
+ throw new BadRequestError("Current password is incorrect");
5047
+ }
5048
+ const hashedPassword = await this.authService.hashPassword(data.newPassword);
5049
+ await this.userService.updatePassword(user.id, hashedPassword);
5050
+ success2(reply, { message: "Password changed successfully" });
5051
+ }
5052
+ };
5053
+ function createAuthController(authService, userService) {
5054
+ return new AuthController(authService, userService);
5055
+ }
5056
+
5057
+ // src/modules/auth/auth.middleware.ts
5058
+ function createAuthMiddleware(authService) {
5059
+ return async function authenticate(request, _reply) {
5060
+ const authHeader = request.headers.authorization;
5061
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
5062
+ throw new UnauthorizedError("Missing or invalid authorization header");
5063
+ }
5064
+ const token = authHeader.substring(7);
5065
+ const payload = await authService.verifyAccessToken(token);
5066
+ request.user = {
5067
+ id: payload.sub,
5068
+ email: payload.email,
5069
+ role: payload.role
5070
+ };
5071
+ };
5072
+ }
5073
+ function createRoleMiddleware(allowedRoles) {
5074
+ return async function authorize(request, _reply) {
5075
+ const user = request.user;
5076
+ if (!user) {
5077
+ throw new UnauthorizedError("Authentication required");
5078
+ }
5079
+ if (!allowedRoles.includes(user.role)) {
5080
+ throw new ForbiddenError("Insufficient permissions");
5081
+ }
5082
+ };
5083
+ }
5084
+
5085
+ // src/modules/auth/auth.routes.ts
5086
+ function registerAuthRoutes(app, controller, authService) {
5087
+ const authenticate = createAuthMiddleware(authService);
5088
+ app.post("/auth/register", controller.register.bind(controller));
5089
+ app.post("/auth/login", controller.login.bind(controller));
5090
+ app.post("/auth/refresh", controller.refresh.bind(controller));
5091
+ app.post("/auth/logout", { preHandler: [authenticate] }, controller.logout.bind(controller));
5092
+ app.get("/auth/me", { preHandler: [authenticate] }, controller.me.bind(controller));
5093
+ app.post(
5094
+ "/auth/change-password",
5095
+ { preHandler: [authenticate] },
5096
+ controller.changePassword.bind(controller)
5097
+ );
5098
+ }
5099
+
5100
+ // src/database/prisma.ts
5101
+ var import_client = require("@prisma/client");
5102
+ var prismaClientSingleton = () => {
5103
+ return new import_client.PrismaClient({
5104
+ log: isProduction() ? ["error"] : ["query", "info", "warn", "error"],
5105
+ errorFormat: isProduction() ? "minimal" : "pretty"
5106
+ });
5107
+ };
5108
+ var prisma = globalThis.__prisma ?? prismaClientSingleton();
5109
+ if (!isProduction()) {
5110
+ globalThis.__prisma = prisma;
5111
+ }
5112
+
5113
+ // src/utils/pagination.ts
5114
+ var DEFAULT_PAGE = 1;
5115
+ var DEFAULT_LIMIT = 20;
5116
+ var MAX_LIMIT = 100;
5117
+ function parsePaginationParams(query) {
5118
+ const page = Math.max(1, parseInt(String(query.page || DEFAULT_PAGE), 10));
5119
+ const limit = Math.min(
5120
+ MAX_LIMIT,
5121
+ Math.max(1, parseInt(String(query.limit || DEFAULT_LIMIT), 10))
5122
+ );
5123
+ const sortBy = typeof query.sortBy === "string" ? query.sortBy : void 0;
5124
+ const sortOrder = query.sortOrder === "desc" ? "desc" : "asc";
5125
+ return { page, limit, sortBy, sortOrder };
5126
+ }
5127
+ function createPaginatedResult(data, total, params) {
5128
+ const totalPages = Math.ceil(total / params.limit);
5129
+ return {
5130
+ data,
5131
+ meta: {
5132
+ total,
5133
+ page: params.page,
5134
+ limit: params.limit,
5135
+ totalPages,
5136
+ hasNextPage: params.page < totalPages,
5137
+ hasPrevPage: params.page > 1
5138
+ }
5139
+ };
5140
+ }
5141
+ function getSkip(params) {
5142
+ return (params.page - 1) * params.limit;
5143
+ }
5144
+
5145
+ // src/modules/user/user.repository.ts
5146
+ var import_client2 = require("@prisma/client");
5147
+ var UserRepository = class {
5148
+ /**
5149
+ * Find user by ID
5150
+ */
5151
+ async findById(id) {
5152
+ const user = await prisma.user.findUnique({
5153
+ where: { id }
5154
+ });
5155
+ if (!user) return null;
5156
+ return this.mapPrismaUserToUser(user);
5157
+ }
5158
+ /**
5159
+ * Find user by email (case-insensitive)
5160
+ */
5161
+ async findByEmail(email) {
5162
+ const user = await prisma.user.findUnique({
5163
+ where: { email: email.toLowerCase() }
5164
+ });
5165
+ if (!user) return null;
5166
+ return this.mapPrismaUserToUser(user);
5167
+ }
5168
+ /**
5169
+ * Find multiple users with pagination and filters
5170
+ */
5171
+ async findMany(params, filters) {
5172
+ const where = this.buildWhereClause(filters);
5173
+ const orderBy = this.buildOrderBy(params);
5174
+ const [data, total] = await Promise.all([
5175
+ prisma.user.findMany({
5176
+ where,
5177
+ orderBy,
5178
+ skip: getSkip(params),
5179
+ take: params.limit
5180
+ }),
5181
+ prisma.user.count({ where })
5182
+ ]);
5183
+ const mappedUsers = data.map((user) => this.mapPrismaUserToUser(user));
5184
+ return createPaginatedResult(mappedUsers, total, params);
5185
+ }
5186
+ /**
5187
+ * Create a new user
5188
+ */
5189
+ async create(data) {
5190
+ const user = await prisma.user.create({
5191
+ data: {
5192
+ email: data.email.toLowerCase(),
5193
+ password: data.password,
5194
+ name: data.name,
5195
+ role: this.mapRoleToEnum(data.role || "user"),
5196
+ status: import_client2.UserStatus.ACTIVE,
5197
+ emailVerified: false
5198
+ }
5199
+ });
5200
+ return this.mapPrismaUserToUser(user);
5201
+ }
5202
+ /**
5203
+ * Update user data
5204
+ */
5205
+ async update(id, data) {
5206
+ try {
5207
+ const user = await prisma.user.update({
5208
+ where: { id },
5209
+ data: {
5210
+ ...data.email && { email: data.email.toLowerCase() },
5211
+ ...data.name !== void 0 && { name: data.name },
5212
+ ...data.role && { role: this.mapRoleToEnum(data.role) },
5213
+ ...data.status && { status: this.mapStatusToEnum(data.status) },
5214
+ ...data.emailVerified !== void 0 && { emailVerified: data.emailVerified },
5215
+ ...data.metadata && { metadata: data.metadata }
5216
+ }
5217
+ });
5218
+ return this.mapPrismaUserToUser(user);
5219
+ } catch {
5220
+ return null;
5221
+ }
5222
+ }
5223
+ /**
5224
+ * Update user password
5225
+ */
5226
+ async updatePassword(id, password) {
5227
+ try {
5228
+ const user = await prisma.user.update({
5229
+ where: { id },
5230
+ data: { password }
5231
+ });
5232
+ return this.mapPrismaUserToUser(user);
5233
+ } catch {
5234
+ return null;
5235
+ }
5236
+ }
5237
+ /**
5238
+ * Update last login timestamp
5239
+ */
5240
+ async updateLastLogin(id) {
5241
+ try {
5242
+ const user = await prisma.user.update({
5243
+ where: { id },
5244
+ data: { lastLoginAt: /* @__PURE__ */ new Date() }
5245
+ });
5246
+ return this.mapPrismaUserToUser(user);
5247
+ } catch {
5248
+ return null;
5249
+ }
5250
+ }
5251
+ /**
5252
+ * Delete user by ID
5253
+ */
5254
+ async delete(id) {
5255
+ try {
5256
+ await prisma.user.delete({
5257
+ where: { id }
5258
+ });
5259
+ return true;
5260
+ } catch {
5261
+ return false;
5262
+ }
5263
+ }
5264
+ /**
5265
+ * Count users with optional filters
5266
+ */
5267
+ async count(filters) {
5268
+ const where = this.buildWhereClause(filters);
5269
+ return prisma.user.count({ where });
5270
+ }
5271
+ /**
5272
+ * Helper to clear all users (for testing only)
5273
+ * WARNING: This deletes all users from the database
5274
+ */
5275
+ async clear() {
5276
+ await prisma.user.deleteMany();
5277
+ }
5278
+ /**
5279
+ * Build Prisma where clause from filters
5280
+ */
5281
+ buildWhereClause(filters) {
5282
+ if (!filters) return {};
5283
+ return {
5284
+ ...filters.status && { status: this.mapStatusToEnum(filters.status) },
5285
+ ...filters.role && { role: this.mapRoleToEnum(filters.role) },
5286
+ ...filters.emailVerified !== void 0 && { emailVerified: filters.emailVerified },
5287
+ ...filters.search && {
5288
+ OR: [
5289
+ { email: { contains: filters.search, mode: "insensitive" } },
5290
+ { name: { contains: filters.search, mode: "insensitive" } }
5291
+ ]
5292
+ }
5293
+ };
5294
+ }
5295
+ /**
5296
+ * Build Prisma orderBy clause from pagination params
5297
+ */
5298
+ buildOrderBy(params) {
5299
+ if (!params.sortBy) {
5300
+ return { createdAt: "desc" };
5301
+ }
5302
+ return {
5303
+ [params.sortBy]: params.sortOrder || "asc"
5304
+ };
5305
+ }
5306
+ /**
5307
+ * Map Prisma User to application User type
5308
+ */
5309
+ mapPrismaUserToUser(prismaUser) {
5310
+ return {
5311
+ id: prismaUser.id,
5312
+ email: prismaUser.email,
5313
+ password: prismaUser.password,
5314
+ name: prismaUser.name ?? void 0,
5315
+ role: this.mapEnumToRole(prismaUser.role),
5316
+ status: this.mapEnumToStatus(prismaUser.status),
5317
+ emailVerified: prismaUser.emailVerified,
5318
+ lastLoginAt: prismaUser.lastLoginAt ?? void 0,
5319
+ metadata: prismaUser.metadata,
5320
+ createdAt: prismaUser.createdAt,
5321
+ updatedAt: prismaUser.updatedAt
5322
+ };
5323
+ }
5324
+ /**
5325
+ * Map application role to Prisma enum
5326
+ */
5327
+ mapRoleToEnum(role) {
5328
+ const roleMap = {
5329
+ user: import_client2.UserRole.USER,
5330
+ moderator: import_client2.UserRole.MODERATOR,
5331
+ admin: import_client2.UserRole.ADMIN,
5332
+ super_admin: import_client2.UserRole.SUPER_ADMIN
5333
+ };
5334
+ return roleMap[role] || import_client2.UserRole.USER;
5335
+ }
5336
+ /**
5337
+ * Map Prisma enum to application role
5338
+ */
5339
+ mapEnumToRole(role) {
5340
+ const roleMap = {
5341
+ [import_client2.UserRole.USER]: "user",
5342
+ [import_client2.UserRole.MODERATOR]: "moderator",
5343
+ [import_client2.UserRole.ADMIN]: "admin",
5344
+ [import_client2.UserRole.SUPER_ADMIN]: "super_admin"
5345
+ };
5346
+ return roleMap[role];
5347
+ }
5348
+ /**
5349
+ * Map application status to Prisma enum
5350
+ */
5351
+ mapStatusToEnum(status) {
5352
+ const statusMap = {
5353
+ active: import_client2.UserStatus.ACTIVE,
5354
+ inactive: import_client2.UserStatus.INACTIVE,
5355
+ suspended: import_client2.UserStatus.SUSPENDED,
5356
+ banned: import_client2.UserStatus.BANNED
5357
+ };
5358
+ return statusMap[status] || import_client2.UserStatus.ACTIVE;
5359
+ }
5360
+ /**
5361
+ * Map Prisma enum to application status
5362
+ */
5363
+ mapEnumToStatus(status) {
5364
+ const statusMap = {
5365
+ [import_client2.UserStatus.ACTIVE]: "active",
5366
+ [import_client2.UserStatus.INACTIVE]: "inactive",
5367
+ [import_client2.UserStatus.SUSPENDED]: "suspended",
5368
+ [import_client2.UserStatus.BANNED]: "banned"
5369
+ };
5370
+ return statusMap[status];
5371
+ }
5372
+ };
5373
+ function createUserRepository() {
5374
+ return new UserRepository();
5375
+ }
5376
+
5377
+ // src/modules/user/types.ts
5378
+ var DEFAULT_ROLE_PERMISSIONS = {
5379
+ user: ["profile:read", "profile:update"],
5380
+ moderator: [
5381
+ "profile:read",
5382
+ "profile:update",
5383
+ "users:read",
5384
+ "content:read",
5385
+ "content:update",
5386
+ "content:delete"
5387
+ ],
5388
+ admin: [
5389
+ "profile:read",
5390
+ "profile:update",
5391
+ "users:read",
5392
+ "users:update",
5393
+ "users:delete",
5394
+ "content:manage",
5395
+ "settings:read"
5396
+ ],
5397
+ super_admin: ["*:manage"]
5398
+ // All permissions
5399
+ };
5400
+
5401
+ // src/modules/user/user.service.ts
5402
+ var UserService = class {
5403
+ constructor(repository) {
5404
+ this.repository = repository;
5405
+ }
5406
+ async findById(id) {
5407
+ return this.repository.findById(id);
5408
+ }
5409
+ async findByEmail(email) {
5410
+ return this.repository.findByEmail(email);
5411
+ }
5412
+ async findMany(params, filters) {
5413
+ const result = await this.repository.findMany(params, filters);
5414
+ return {
5415
+ ...result,
5416
+ data: result.data.map(({ password: _password, ...user }) => user)
5417
+ };
5418
+ }
5419
+ async create(data) {
5420
+ const existing = await this.repository.findByEmail(data.email);
5421
+ if (existing) {
5422
+ throw new ConflictError("User with this email already exists");
5423
+ }
5424
+ const user = await this.repository.create(data);
5425
+ logger.info({ userId: user.id, email: user.email }, "User created");
5426
+ return user;
5427
+ }
5428
+ async update(id, data) {
5429
+ const user = await this.repository.findById(id);
5430
+ if (!user) {
5431
+ throw new NotFoundError("User");
5432
+ }
5433
+ if (data.email && data.email !== user.email) {
5434
+ const existing = await this.repository.findByEmail(data.email);
5435
+ if (existing) {
5436
+ throw new ConflictError("Email already in use");
5437
+ }
5438
+ }
5439
+ const updatedUser = await this.repository.update(id, data);
5440
+ if (!updatedUser) {
5441
+ throw new NotFoundError("User");
5442
+ }
5443
+ logger.info({ userId: id }, "User updated");
5444
+ return updatedUser;
5445
+ }
5446
+ async updatePassword(id, hashedPassword) {
5447
+ const user = await this.repository.updatePassword(id, hashedPassword);
5448
+ if (!user) {
5449
+ throw new NotFoundError("User");
5450
+ }
5451
+ logger.info({ userId: id }, "User password updated");
5452
+ return user;
5453
+ }
5454
+ async updateLastLogin(id) {
5455
+ const user = await this.repository.updateLastLogin(id);
5456
+ if (!user) {
5457
+ throw new NotFoundError("User");
5458
+ }
5459
+ return user;
5460
+ }
5461
+ async delete(id) {
5462
+ const user = await this.repository.findById(id);
5463
+ if (!user) {
5464
+ throw new NotFoundError("User");
5465
+ }
5466
+ await this.repository.delete(id);
5467
+ logger.info({ userId: id }, "User deleted");
5468
+ }
5469
+ async suspend(id) {
5470
+ return this.update(id, { status: "suspended" });
5471
+ }
5472
+ async ban(id) {
5473
+ return this.update(id, { status: "banned" });
5474
+ }
5475
+ async activate(id) {
5476
+ return this.update(id, { status: "active" });
5477
+ }
5478
+ async verifyEmail(id) {
5479
+ return this.update(id, { emailVerified: true });
5480
+ }
5481
+ async changeRole(id, role) {
5482
+ return this.update(id, { role });
5483
+ }
5484
+ // RBAC helpers
5485
+ hasPermission(role, permission) {
5486
+ const permissions = DEFAULT_ROLE_PERMISSIONS[role] || [];
5487
+ if (permissions.includes("*:manage")) {
5488
+ return true;
5489
+ }
5490
+ if (permissions.includes(permission)) {
5491
+ return true;
5492
+ }
5493
+ const [resource] = permission.split(":");
5494
+ const managePermission = `${resource}:manage`;
5495
+ if (permissions.includes(managePermission)) {
5496
+ return true;
5497
+ }
5498
+ return false;
5499
+ }
5500
+ getPermissions(role) {
5501
+ return DEFAULT_ROLE_PERMISSIONS[role] || [];
5502
+ }
5503
+ };
5504
+ function createUserService(repository) {
5505
+ return new UserService(repository || createUserRepository());
5506
+ }
5507
+
5508
+ // src/modules/auth/index.ts
5509
+ async function registerAuthModule(app) {
5510
+ await app.register(import_jwt.default, {
5511
+ secret: config.jwt.secret,
5512
+ sign: {
5513
+ algorithm: "HS256"
5514
+ }
5515
+ });
5516
+ await app.register(import_cookie.default, {
5517
+ secret: config.jwt.secret,
5518
+ hook: "onRequest"
5519
+ });
5520
+ const authService = createAuthService(app);
5521
+ const userService = createUserService();
5522
+ const authController = createAuthController(authService, userService);
5523
+ registerAuthRoutes(app, authController, authService);
5524
+ logger.info("Auth module registered");
5525
+ }
5526
+
5527
+ // src/modules/user/schemas.ts
5528
+ var import_zod4 = require("zod");
5529
+ var userStatusEnum = import_zod4.z.enum(["active", "inactive", "suspended", "banned"]);
5530
+ var userRoleEnum = import_zod4.z.enum(["user", "admin", "moderator", "super_admin"]);
5531
+ var createUserSchema = import_zod4.z.object({
5532
+ email: import_zod4.z.string().email("Invalid email address"),
5533
+ password: import_zod4.z.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number"),
5534
+ name: import_zod4.z.string().min(2, "Name must be at least 2 characters").optional(),
5535
+ role: userRoleEnum.optional().default("user")
5536
+ });
5537
+ var updateUserSchema = import_zod4.z.object({
5538
+ email: import_zod4.z.string().email("Invalid email address").optional(),
5539
+ name: import_zod4.z.string().min(2, "Name must be at least 2 characters").optional(),
5540
+ role: userRoleEnum.optional(),
5541
+ status: userStatusEnum.optional(),
5542
+ emailVerified: import_zod4.z.boolean().optional(),
5543
+ metadata: import_zod4.z.record(import_zod4.z.unknown()).optional()
5544
+ });
5545
+ var updateProfileSchema = import_zod4.z.object({
5546
+ name: import_zod4.z.string().min(2, "Name must be at least 2 characters").optional(),
5547
+ metadata: import_zod4.z.record(import_zod4.z.unknown()).optional()
5548
+ });
5549
+ var userQuerySchema = import_zod4.z.object({
5550
+ page: import_zod4.z.string().transform(Number).optional(),
5551
+ limit: import_zod4.z.string().transform(Number).optional(),
5552
+ sortBy: import_zod4.z.string().optional(),
5553
+ sortOrder: import_zod4.z.enum(["asc", "desc"]).optional(),
5554
+ status: userStatusEnum.optional(),
5555
+ role: userRoleEnum.optional(),
5556
+ search: import_zod4.z.string().optional(),
5557
+ emailVerified: import_zod4.z.string().transform((val) => val === "true").optional()
5558
+ });
5559
+
5560
+ // src/modules/user/user.controller.ts
5561
+ function omitPassword(user) {
5562
+ const { password, ...userData } = user;
5563
+ void password;
5564
+ return userData;
5565
+ }
5566
+ var UserController = class {
5567
+ constructor(userService) {
5568
+ this.userService = userService;
5569
+ }
5570
+ async list(request, reply) {
5571
+ const query = validateQuery(userQuerySchema, request.query);
5572
+ const pagination = parsePaginationParams(query);
5573
+ const filters = {
5574
+ status: query.status,
5575
+ role: query.role,
5576
+ search: query.search,
5577
+ emailVerified: query.emailVerified
5578
+ };
5579
+ const result = await this.userService.findMany(pagination, filters);
5580
+ success2(reply, result);
5581
+ }
5582
+ async getById(request, reply) {
5583
+ const user = await this.userService.findById(request.params.id);
5584
+ if (!user) {
5585
+ return reply.status(404).send({
5586
+ success: false,
5587
+ message: "User not found"
5588
+ });
5589
+ }
5590
+ success2(reply, omitPassword(user));
5591
+ }
5592
+ async update(request, reply) {
5593
+ const data = validateBody(updateUserSchema, request.body);
5594
+ const user = await this.userService.update(request.params.id, data);
5595
+ success2(reply, omitPassword(user));
5596
+ }
5597
+ async delete(request, reply) {
5598
+ const authRequest = request;
5599
+ if (authRequest.user.id === request.params.id) {
5600
+ throw new ForbiddenError("Cannot delete your own account");
5601
+ }
5602
+ await this.userService.delete(request.params.id);
5603
+ noContent(reply);
5604
+ }
5605
+ async suspend(request, reply) {
5606
+ const authRequest = request;
5607
+ if (authRequest.user.id === request.params.id) {
5608
+ throw new ForbiddenError("Cannot suspend your own account");
5609
+ }
5610
+ const user = await this.userService.suspend(request.params.id);
5611
+ const userData = omitPassword(user);
5612
+ success2(reply, userData);
5613
+ }
5614
+ async ban(request, reply) {
5615
+ const authRequest = request;
5616
+ if (authRequest.user.id === request.params.id) {
5617
+ throw new ForbiddenError("Cannot ban your own account");
5618
+ }
5619
+ const user = await this.userService.ban(request.params.id);
5620
+ const userData = omitPassword(user);
5621
+ success2(reply, userData);
5622
+ }
5623
+ async activate(request, reply) {
5624
+ const user = await this.userService.activate(request.params.id);
5625
+ const userData = omitPassword(user);
5626
+ success2(reply, userData);
5627
+ }
5628
+ // Profile routes (for authenticated user)
5629
+ async getProfile(request, reply) {
5630
+ const authRequest = request;
5631
+ const user = await this.userService.findById(authRequest.user.id);
5632
+ if (!user) {
5633
+ return reply.status(404).send({
5634
+ success: false,
5635
+ message: "User not found"
5636
+ });
5637
+ }
5638
+ const userData = omitPassword(user);
5639
+ success2(reply, userData);
5640
+ }
5641
+ async updateProfile(request, reply) {
5642
+ const authRequest = request;
5643
+ const data = validateBody(updateProfileSchema, request.body);
5644
+ const user = await this.userService.update(authRequest.user.id, data);
5645
+ const userData = omitPassword(user);
5646
+ success2(reply, userData);
5647
+ }
5648
+ };
5649
+ function createUserController(userService) {
5650
+ return new UserController(userService);
5651
+ }
5652
+
5653
+ // src/modules/user/user.routes.ts
5654
+ var idParamsSchema = {
5655
+ type: "object",
5656
+ properties: {
5657
+ id: { type: "string" }
5658
+ },
5659
+ required: ["id"]
5660
+ };
5661
+ function registerUserRoutes(app, controller, authService) {
5662
+ const authenticate = createAuthMiddleware(authService);
5663
+ const isAdmin = createRoleMiddleware(["admin", "super_admin"]);
5664
+ const isModerator = createRoleMiddleware(["moderator", "admin", "super_admin"]);
5665
+ app.get("/profile", { preHandler: [authenticate] }, controller.getProfile.bind(controller));
5666
+ app.patch("/profile", { preHandler: [authenticate] }, controller.updateProfile.bind(controller));
5667
+ app.get("/users", { preHandler: [authenticate, isModerator] }, controller.list.bind(controller));
5668
+ app.get(
5669
+ "/users/:id",
5670
+ { preHandler: [authenticate, isModerator], schema: { params: idParamsSchema } },
5671
+ async (request, reply) => {
5672
+ return controller.getById(request, reply);
5673
+ }
5674
+ );
5675
+ app.patch(
5676
+ "/users/:id",
5677
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
5678
+ async (request, reply) => {
5679
+ return controller.update(request, reply);
5680
+ }
5681
+ );
5682
+ app.delete(
5683
+ "/users/:id",
5684
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
5685
+ async (request, reply) => {
5686
+ return controller.delete(request, reply);
5687
+ }
5688
+ );
5689
+ app.post(
5690
+ "/users/:id/suspend",
5691
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
5692
+ async (request, reply) => {
5693
+ return controller.suspend(request, reply);
5694
+ }
5695
+ );
5696
+ app.post(
5697
+ "/users/:id/ban",
5698
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
5699
+ async (request, reply) => {
5700
+ return controller.ban(request, reply);
5701
+ }
5702
+ );
5703
+ app.post(
5704
+ "/users/:id/activate",
5705
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
5706
+ async (request, reply) => {
5707
+ return controller.activate(request, reply);
5708
+ }
5709
+ );
5710
+ }
5711
+
5712
+ // src/modules/user/index.ts
5713
+ async function registerUserModule(app, authService) {
5714
+ const repository = createUserRepository();
5715
+ const userService = createUserService(repository);
5716
+ const userController = createUserController(userService);
5717
+ registerUserRoutes(app, userController, authService);
5718
+ logger.info("User module registered");
5719
+ }
5720
+
5721
+ // src/cli/utils/docs-generator.ts
5722
+ async function generateDocs(outputPath = "openapi.json", silent = false) {
5723
+ const spinner = silent ? null : (0, import_ora5.default)("Generating OpenAPI documentation...").start();
5724
+ try {
5725
+ const server = createServer({
5726
+ port: config.server.port,
5727
+ host: config.server.host
5728
+ });
5729
+ const app = server.instance;
5730
+ registerErrorHandler(app);
5731
+ await registerSecurity(app);
5732
+ await registerSwagger(app, {
5733
+ title: "Servcraft API",
5734
+ description: "API documentation generated by Servcraft",
5735
+ version: "1.0.0"
5736
+ });
5737
+ await registerAuthModule(app);
5738
+ const authService = createAuthService(app);
5739
+ await registerUserModule(app, authService);
5740
+ await app.ready();
5741
+ const spec = app.swagger();
5742
+ const absoluteOutput = import_path5.default.resolve(outputPath);
5743
+ await import_promises3.default.mkdir(import_path5.default.dirname(absoluteOutput), { recursive: true });
5744
+ await import_promises3.default.writeFile(absoluteOutput, JSON.stringify(spec, null, 2), "utf8");
5745
+ spinner?.succeed(`OpenAPI spec generated at ${absoluteOutput}`);
5746
+ await app.close();
5747
+ return absoluteOutput;
5748
+ } catch (error2) {
5749
+ spinner?.fail("Failed to generate OpenAPI documentation");
5750
+ throw error2;
5751
+ }
5752
+ }
5753
+
5754
+ // src/cli/commands/docs.ts
5755
+ var docsCommand = new import_commander5.Command("docs").description("API documentation commands");
5756
+ docsCommand.command("generate").alias("gen").description("Generate OpenAPI/Swagger documentation").option("-o, --output <file>", "Output file path", "openapi.json").option("-f, --format <format>", "Output format: json, yaml", "json").action(async (options) => {
5757
+ try {
5758
+ const outputPath = await generateDocs(options.output, false);
5759
+ if (options.format === "yaml") {
5760
+ const jsonContent = await import_promises4.default.readFile(outputPath, "utf-8");
5761
+ const spec = JSON.parse(jsonContent);
5762
+ const yamlPath = outputPath.replace(".json", ".yaml");
5763
+ await import_promises4.default.writeFile(yamlPath, jsonToYaml(spec));
5764
+ success(`YAML documentation generated: ${yamlPath}`);
5765
+ }
5766
+ console.log("\n\u{1F4DA} Documentation URLs:");
5767
+ info(" Swagger UI: http://localhost:3000/docs");
5768
+ info(" OpenAPI JSON: http://localhost:3000/docs/json");
5769
+ } catch (err) {
5770
+ error(err instanceof Error ? err.message : String(err));
5771
+ }
5772
+ });
5773
+ docsCommand.option("-o, --output <path>", "Output file path", "openapi.json").action(async (options) => {
5774
+ if (options.output) {
5775
+ try {
5776
+ const outputPath = await generateDocs(options.output);
5777
+ success(`Documentation written to ${outputPath}`);
5778
+ } catch (err) {
5779
+ error(err instanceof Error ? err.message : String(err));
5780
+ process.exitCode = 1;
5781
+ }
5782
+ }
5783
+ });
5784
+ docsCommand.command("export").description("Export documentation to Postman, Insomnia, or YAML").option("-f, --format <format>", "Export format: postman, insomnia, yaml", "postman").option("-o, --output <file>", "Output file path").action(async (options) => {
5785
+ const spinner = (0, import_ora6.default)("Exporting documentation...").start();
5786
+ try {
5787
+ const projectRoot = getProjectRoot();
5788
+ const specPath = import_path6.default.join(projectRoot, "openapi.json");
5789
+ try {
5790
+ await import_promises4.default.access(specPath);
5791
+ } catch {
5792
+ spinner.text = "Generating OpenAPI spec first...";
5793
+ await generateDocs("openapi.json", true);
5794
+ }
5795
+ const specContent = await import_promises4.default.readFile(specPath, "utf-8");
5796
+ const spec = JSON.parse(specContent);
5797
+ let output;
5798
+ let defaultName;
5799
+ switch (options.format) {
5800
+ case "postman":
5801
+ output = JSON.stringify(convertToPostman(spec), null, 2);
5802
+ defaultName = "postman_collection.json";
5803
+ break;
5804
+ case "insomnia":
5805
+ output = JSON.stringify(convertToInsomnia(spec), null, 2);
5806
+ defaultName = "insomnia_collection.json";
5807
+ break;
5808
+ case "yaml":
5809
+ output = jsonToYaml(spec);
5810
+ defaultName = "openapi.yaml";
5811
+ break;
5812
+ default:
5813
+ throw new Error(`Unknown format: ${options.format}`);
5814
+ }
5815
+ const outPath = import_path6.default.join(projectRoot, options.output || defaultName);
5816
+ await import_promises4.default.writeFile(outPath, output);
5817
+ spinner.succeed(`Exported to: ${options.output || defaultName}`);
5818
+ if (options.format === "postman") {
5819
+ info("\n Import in Postman: File > Import > Select file");
5820
+ }
5821
+ } catch (err) {
5822
+ spinner.fail("Export failed");
5823
+ error(err instanceof Error ? err.message : String(err));
5824
+ }
5825
+ });
5826
+ docsCommand.command("status").description("Show documentation status").action(async () => {
5827
+ const projectRoot = getProjectRoot();
5828
+ console.log(import_chalk6.default.bold("\n\u{1F4CA} Documentation Status\n"));
5829
+ const specPath = import_path6.default.join(projectRoot, "openapi.json");
5830
+ try {
5831
+ const stat2 = await import_promises4.default.stat(specPath);
5832
+ success(
5833
+ `openapi.json exists (${formatBytes(stat2.size)}, modified ${formatDate(stat2.mtime)})`
5834
+ );
5835
+ const content = await import_promises4.default.readFile(specPath, "utf-8");
5836
+ const spec = JSON.parse(content);
5837
+ const pathCount = Object.keys(spec.paths || {}).length;
5838
+ info(` ${pathCount} endpoints documented`);
5839
+ } catch {
5840
+ warn('openapi.json not found - run "servcraft docs generate"');
5841
+ }
5842
+ console.log("\n\u{1F4CC} Commands:");
5843
+ info(" servcraft docs generate Generate OpenAPI spec");
5844
+ info(" servcraft docs export Export to Postman/Insomnia");
5845
+ });
5846
+ function jsonToYaml(obj, indent = 0) {
5847
+ const spaces = " ".repeat(indent);
5848
+ if (obj === null || obj === void 0) return "null";
5849
+ if (typeof obj === "string") {
5850
+ if (obj.includes("\n") || obj.includes(":") || obj.includes("#")) {
5851
+ return `"${obj.replace(/"/g, '\\"')}"`;
5852
+ }
5853
+ return obj || '""';
5854
+ }
5855
+ if (typeof obj === "number" || typeof obj === "boolean") return String(obj);
5856
+ if (Array.isArray(obj)) {
5857
+ if (obj.length === 0) return "[]";
5858
+ return obj.map((item) => `${spaces}- ${jsonToYaml(item, indent + 1).trimStart()}`).join("\n");
5859
+ }
5860
+ if (typeof obj === "object") {
5861
+ const entries = Object.entries(obj);
5862
+ if (entries.length === 0) return "{}";
5863
+ return entries.map(([key, value]) => {
5864
+ const valueStr = jsonToYaml(value, indent + 1);
5865
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0) {
5866
+ return `${spaces}${key}:
5867
+ ${valueStr}`;
5868
+ }
5869
+ return `${spaces}${key}: ${valueStr}`;
5870
+ }).join("\n");
5871
+ }
5872
+ return String(obj);
5873
+ }
5874
+ function convertToPostman(spec) {
5875
+ const baseUrl = spec.servers?.[0]?.url || "http://localhost:3000";
5876
+ const items = [];
5877
+ for (const [pathUrl, methods] of Object.entries(spec.paths || {})) {
5878
+ for (const [method, details] of Object.entries(methods)) {
5879
+ items.push({
5880
+ name: details.summary || `${method.toUpperCase()} ${pathUrl}`,
5881
+ request: {
5882
+ method: method.toUpperCase(),
5883
+ header: [
5884
+ { key: "Content-Type", value: "application/json" },
5885
+ { key: "Authorization", value: "Bearer {{token}}" }
5886
+ ],
5887
+ url: {
5888
+ raw: `{{baseUrl}}${pathUrl}`,
5889
+ host: ["{{baseUrl}}"],
5890
+ path: pathUrl.split("/").filter(Boolean)
5891
+ },
5892
+ ...details.requestBody ? { body: { mode: "raw", raw: "{}" } } : {}
5893
+ }
5894
+ });
5895
+ }
5896
+ }
5897
+ return {
5898
+ info: {
5899
+ name: spec.info.title,
5900
+ schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
5901
+ },
5902
+ item: items,
5903
+ variable: [
5904
+ { key: "baseUrl", value: baseUrl },
5905
+ { key: "token", value: "" }
5906
+ ]
5907
+ };
5908
+ }
5909
+ function convertToInsomnia(spec) {
5910
+ const baseUrl = spec.servers?.[0]?.url || "http://localhost:3000";
5911
+ const resources = [
5912
+ { _type: "environment", name: "Base Environment", data: { baseUrl, token: "" } }
5913
+ ];
5914
+ for (const [pathUrl, methods] of Object.entries(spec.paths || {})) {
5915
+ for (const [method, details] of Object.entries(methods)) {
5916
+ resources.push({
5917
+ _type: "request",
5918
+ name: details.summary || `${method.toUpperCase()} ${pathUrl}`,
5919
+ method: method.toUpperCase(),
5920
+ url: `{{ baseUrl }}${pathUrl}`,
5921
+ headers: [
5922
+ { name: "Content-Type", value: "application/json" },
5923
+ { name: "Authorization", value: "Bearer {{ token }}" }
5924
+ ]
5925
+ });
5926
+ }
5927
+ }
5928
+ return { _type: "export", __export_format: 4, resources };
5929
+ }
5930
+ function formatBytes(bytes) {
5931
+ if (bytes < 1024) return `${bytes} B`;
5932
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
5933
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
5934
+ }
5935
+ function formatDate(date) {
5936
+ return date.toLocaleDateString("en-US", {
5937
+ month: "short",
5938
+ day: "numeric",
5939
+ hour: "2-digit",
5940
+ minute: "2-digit"
5941
+ });
5942
+ }
5943
+
3854
5944
  // src/cli/index.ts
3855
- var program = new import_commander5.Command();
5945
+ var program = new import_commander6.Command();
3856
5946
  program.name("servcraft").description("Servcraft - A modular Node.js backend framework CLI").version("0.1.0");
3857
5947
  program.addCommand(initCommand);
3858
5948
  program.addCommand(generateCommand);
3859
5949
  program.addCommand(addModuleCommand);
3860
5950
  program.addCommand(dbCommand);
5951
+ program.addCommand(docsCommand);
3861
5952
  program.parse();
3862
5953
  //# sourceMappingURL=index.cjs.map