servcraft 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/.github/CODEOWNERS +18 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
  4. package/.github/dependabot.yml +59 -0
  5. package/.github/workflows/ci.yml +188 -0
  6. package/.github/workflows/release.yml +195 -0
  7. package/AUDIT.md +602 -0
  8. package/LICENSE +21 -0
  9. package/README.md +1102 -1
  10. package/dist/cli/index.cjs +2026 -2168
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +2026 -2168
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/index.cjs +595 -616
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +114 -52
  17. package/dist/index.d.ts +114 -52
  18. package/dist/index.js +595 -616
  19. package/dist/index.js.map +1 -1
  20. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  21. package/docs/DATABASE_MULTI_ORM.md +399 -0
  22. package/docs/PHASE1_BREAKDOWN.md +346 -0
  23. package/docs/PROGRESS.md +550 -0
  24. package/docs/modules/ANALYTICS.md +226 -0
  25. package/docs/modules/API-VERSIONING.md +252 -0
  26. package/docs/modules/AUDIT.md +192 -0
  27. package/docs/modules/AUTH.md +431 -0
  28. package/docs/modules/CACHE.md +346 -0
  29. package/docs/modules/EMAIL.md +254 -0
  30. package/docs/modules/FEATURE-FLAG.md +291 -0
  31. package/docs/modules/I18N.md +294 -0
  32. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  33. package/docs/modules/MFA.md +266 -0
  34. package/docs/modules/NOTIFICATION.md +311 -0
  35. package/docs/modules/OAUTH.md +237 -0
  36. package/docs/modules/PAYMENT.md +804 -0
  37. package/docs/modules/QUEUE.md +540 -0
  38. package/docs/modules/RATE-LIMIT.md +339 -0
  39. package/docs/modules/SEARCH.md +288 -0
  40. package/docs/modules/SECURITY.md +327 -0
  41. package/docs/modules/SESSION.md +382 -0
  42. package/docs/modules/SWAGGER.md +305 -0
  43. package/docs/modules/UPLOAD.md +296 -0
  44. package/docs/modules/USER.md +505 -0
  45. package/docs/modules/VALIDATION.md +294 -0
  46. package/docs/modules/WEBHOOK.md +270 -0
  47. package/docs/modules/WEBSOCKET.md +691 -0
  48. package/package.json +53 -38
  49. package/prisma/schema.prisma +395 -1
  50. package/src/cli/commands/add-module.ts +520 -87
  51. package/src/cli/commands/db.ts +3 -4
  52. package/src/cli/commands/docs.ts +256 -6
  53. package/src/cli/commands/generate.ts +12 -19
  54. package/src/cli/commands/init.ts +384 -214
  55. package/src/cli/index.ts +0 -4
  56. package/src/cli/templates/repository.ts +6 -1
  57. package/src/cli/templates/routes.ts +6 -21
  58. package/src/cli/utils/docs-generator.ts +6 -7
  59. package/src/cli/utils/env-manager.ts +717 -0
  60. package/src/cli/utils/field-parser.ts +16 -7
  61. package/src/cli/utils/interactive-prompt.ts +223 -0
  62. package/src/cli/utils/template-manager.ts +346 -0
  63. package/src/config/database.config.ts +183 -0
  64. package/src/config/env.ts +0 -10
  65. package/src/config/index.ts +0 -14
  66. package/src/core/server.ts +1 -1
  67. package/src/database/adapters/mongoose.adapter.ts +132 -0
  68. package/src/database/adapters/prisma.adapter.ts +118 -0
  69. package/src/database/connection.ts +190 -0
  70. package/src/database/interfaces/database.interface.ts +85 -0
  71. package/src/database/interfaces/index.ts +7 -0
  72. package/src/database/interfaces/repository.interface.ts +129 -0
  73. package/src/database/models/mongoose/index.ts +7 -0
  74. package/src/database/models/mongoose/payment.schema.ts +347 -0
  75. package/src/database/models/mongoose/user.schema.ts +154 -0
  76. package/src/database/prisma.ts +1 -4
  77. package/src/database/redis.ts +101 -0
  78. package/src/database/repositories/mongoose/index.ts +7 -0
  79. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  80. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  81. package/src/database/seed.ts +6 -1
  82. package/src/index.ts +9 -20
  83. package/src/middleware/security.ts +2 -6
  84. package/src/modules/analytics/analytics.routes.ts +80 -0
  85. package/src/modules/analytics/analytics.service.ts +364 -0
  86. package/src/modules/analytics/index.ts +18 -0
  87. package/src/modules/analytics/types.ts +180 -0
  88. package/src/modules/api-versioning/index.ts +15 -0
  89. package/src/modules/api-versioning/types.ts +86 -0
  90. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  91. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  92. package/src/modules/api-versioning/versioning.service.ts +189 -0
  93. package/src/modules/audit/audit.repository.ts +206 -0
  94. package/src/modules/audit/audit.service.ts +27 -59
  95. package/src/modules/auth/auth.controller.ts +2 -2
  96. package/src/modules/auth/auth.middleware.ts +3 -9
  97. package/src/modules/auth/auth.routes.ts +10 -107
  98. package/src/modules/auth/auth.service.ts +126 -23
  99. package/src/modules/auth/index.ts +3 -4
  100. package/src/modules/cache/cache.service.ts +367 -0
  101. package/src/modules/cache/index.ts +10 -0
  102. package/src/modules/cache/types.ts +44 -0
  103. package/src/modules/email/email.service.ts +3 -10
  104. package/src/modules/email/templates.ts +2 -8
  105. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  106. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  107. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  108. package/src/modules/feature-flag/index.ts +20 -0
  109. package/src/modules/feature-flag/types.ts +192 -0
  110. package/src/modules/i18n/i18n.middleware.ts +186 -0
  111. package/src/modules/i18n/i18n.routes.ts +191 -0
  112. package/src/modules/i18n/i18n.service.ts +456 -0
  113. package/src/modules/i18n/index.ts +18 -0
  114. package/src/modules/i18n/types.ts +118 -0
  115. package/src/modules/media-processing/index.ts +17 -0
  116. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  117. package/src/modules/media-processing/media-processing.service.ts +245 -0
  118. package/src/modules/media-processing/types.ts +156 -0
  119. package/src/modules/mfa/index.ts +20 -0
  120. package/src/modules/mfa/mfa.repository.ts +206 -0
  121. package/src/modules/mfa/mfa.routes.ts +595 -0
  122. package/src/modules/mfa/mfa.service.ts +572 -0
  123. package/src/modules/mfa/totp.ts +150 -0
  124. package/src/modules/mfa/types.ts +57 -0
  125. package/src/modules/notification/index.ts +20 -0
  126. package/src/modules/notification/notification.repository.ts +356 -0
  127. package/src/modules/notification/notification.service.ts +483 -0
  128. package/src/modules/notification/types.ts +119 -0
  129. package/src/modules/oauth/index.ts +20 -0
  130. package/src/modules/oauth/oauth.repository.ts +219 -0
  131. package/src/modules/oauth/oauth.routes.ts +446 -0
  132. package/src/modules/oauth/oauth.service.ts +293 -0
  133. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  134. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  135. package/src/modules/oauth/providers/github.provider.ts +248 -0
  136. package/src/modules/oauth/providers/google.provider.ts +189 -0
  137. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  138. package/src/modules/oauth/types.ts +94 -0
  139. package/src/modules/payment/index.ts +19 -0
  140. package/src/modules/payment/payment.repository.ts +733 -0
  141. package/src/modules/payment/payment.routes.ts +390 -0
  142. package/src/modules/payment/payment.service.ts +354 -0
  143. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  144. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  145. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  146. package/src/modules/payment/types.ts +140 -0
  147. package/src/modules/queue/cron.ts +438 -0
  148. package/src/modules/queue/index.ts +87 -0
  149. package/src/modules/queue/queue.routes.ts +600 -0
  150. package/src/modules/queue/queue.service.ts +842 -0
  151. package/src/modules/queue/types.ts +222 -0
  152. package/src/modules/queue/workers.ts +366 -0
  153. package/src/modules/rate-limit/index.ts +59 -0
  154. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  155. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  156. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  157. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  158. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  159. package/src/modules/rate-limit/types.ts +153 -0
  160. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  161. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  162. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  163. package/src/modules/search/index.ts +21 -0
  164. package/src/modules/search/search.service.ts +234 -0
  165. package/src/modules/search/types.ts +214 -0
  166. package/src/modules/security/index.ts +40 -0
  167. package/src/modules/security/sanitize.ts +223 -0
  168. package/src/modules/security/security-audit.service.ts +388 -0
  169. package/src/modules/security/security.middleware.ts +398 -0
  170. package/src/modules/session/index.ts +3 -0
  171. package/src/modules/session/session.repository.ts +159 -0
  172. package/src/modules/session/session.service.ts +340 -0
  173. package/src/modules/session/types.ts +38 -0
  174. package/src/modules/swagger/index.ts +7 -1
  175. package/src/modules/swagger/schema-builder.ts +16 -4
  176. package/src/modules/swagger/swagger.service.ts +9 -10
  177. package/src/modules/swagger/types.ts +0 -2
  178. package/src/modules/upload/index.ts +14 -0
  179. package/src/modules/upload/types.ts +83 -0
  180. package/src/modules/upload/upload.repository.ts +199 -0
  181. package/src/modules/upload/upload.routes.ts +311 -0
  182. package/src/modules/upload/upload.service.ts +448 -0
  183. package/src/modules/user/index.ts +3 -3
  184. package/src/modules/user/user.controller.ts +15 -9
  185. package/src/modules/user/user.repository.ts +237 -113
  186. package/src/modules/user/user.routes.ts +39 -164
  187. package/src/modules/user/user.service.ts +4 -3
  188. package/src/modules/validation/validator.ts +12 -17
  189. package/src/modules/webhook/index.ts +91 -0
  190. package/src/modules/webhook/retry.ts +196 -0
  191. package/src/modules/webhook/signature.ts +135 -0
  192. package/src/modules/webhook/types.ts +181 -0
  193. package/src/modules/webhook/webhook.repository.ts +358 -0
  194. package/src/modules/webhook/webhook.routes.ts +442 -0
  195. package/src/modules/webhook/webhook.service.ts +457 -0
  196. package/src/modules/websocket/features.ts +504 -0
  197. package/src/modules/websocket/index.ts +106 -0
  198. package/src/modules/websocket/middlewares.ts +298 -0
  199. package/src/modules/websocket/types.ts +181 -0
  200. package/src/modules/websocket/websocket.service.ts +692 -0
  201. package/src/utils/errors.ts +7 -0
  202. package/src/utils/pagination.ts +4 -1
  203. package/tests/helpers/db-check.ts +79 -0
  204. package/tests/integration/auth-redis.test.ts +94 -0
  205. package/tests/integration/cache-redis.test.ts +387 -0
  206. package/tests/integration/mongoose-repositories.test.ts +410 -0
  207. package/tests/integration/payment-prisma.test.ts +637 -0
  208. package/tests/integration/queue-bullmq.test.ts +417 -0
  209. package/tests/integration/user-prisma.test.ts +441 -0
  210. package/tests/integration/websocket-socketio.test.ts +552 -0
  211. package/tests/setup.ts +11 -9
  212. package/vitest.config.ts +3 -8
  213. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  216. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  217. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
package/dist/index.cjs CHANGED
@@ -286,12 +286,6 @@ var envSchema = import_zod.z.object({
286
286
  SMTP_FROM: import_zod.z.string().optional(),
287
287
  // Redis (optional)
288
288
  REDIS_URL: import_zod.z.string().optional(),
289
- // Swagger/OpenAPI
290
- SWAGGER_ENABLED: import_zod.z.union([import_zod.z.literal("true"), import_zod.z.literal("false")]).default("true").transform((val) => val === "true"),
291
- SWAGGER_ROUTE: import_zod.z.string().default("/docs"),
292
- SWAGGER_TITLE: import_zod.z.string().default("Servcraft API"),
293
- SWAGGER_DESCRIPTION: import_zod.z.string().default("API documentation"),
294
- SWAGGER_VERSION: import_zod.z.string().default("1.0.0"),
295
289
  // Logging
296
290
  LOG_LEVEL: import_zod.z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info")
297
291
  });
@@ -356,13 +350,6 @@ function createConfig() {
356
350
  },
357
351
  redis: {
358
352
  url: env.REDIS_URL
359
- },
360
- swagger: {
361
- enabled: env.SWAGGER_ENABLED,
362
- route: env.SWAGGER_ROUTE,
363
- title: env.SWAGGER_TITLE,
364
- description: env.SWAGGER_DESCRIPTION,
365
- version: env.SWAGGER_VERSION
366
353
  }
367
354
  };
368
355
  }
@@ -382,39 +369,46 @@ var AppError = class _AppError extends Error {
382
369
  Error.captureStackTrace(this, this.constructor);
383
370
  }
384
371
  };
385
- var NotFoundError = class extends AppError {
372
+ var NotFoundError = class _NotFoundError extends AppError {
386
373
  constructor(resource = "Resource") {
387
374
  super(`${resource} not found`, 404);
375
+ Object.setPrototypeOf(this, _NotFoundError.prototype);
388
376
  }
389
377
  };
390
- var UnauthorizedError = class extends AppError {
378
+ var UnauthorizedError = class _UnauthorizedError extends AppError {
391
379
  constructor(message = "Unauthorized") {
392
380
  super(message, 401);
381
+ Object.setPrototypeOf(this, _UnauthorizedError.prototype);
393
382
  }
394
383
  };
395
- var ForbiddenError = class extends AppError {
384
+ var ForbiddenError = class _ForbiddenError extends AppError {
396
385
  constructor(message = "Forbidden") {
397
386
  super(message, 403);
387
+ Object.setPrototypeOf(this, _ForbiddenError.prototype);
398
388
  }
399
389
  };
400
- var BadRequestError = class extends AppError {
390
+ var BadRequestError = class _BadRequestError extends AppError {
401
391
  constructor(message = "Bad request", errors) {
402
392
  super(message, 400, true, errors);
393
+ Object.setPrototypeOf(this, _BadRequestError.prototype);
403
394
  }
404
395
  };
405
- var ConflictError = class extends AppError {
396
+ var ConflictError = class _ConflictError extends AppError {
406
397
  constructor(message = "Resource already exists") {
407
398
  super(message, 409);
399
+ Object.setPrototypeOf(this, _ConflictError.prototype);
408
400
  }
409
401
  };
410
- var ValidationError = class extends AppError {
402
+ var ValidationError = class _ValidationError extends AppError {
411
403
  constructor(errors) {
412
404
  super("Validation failed", 422, true, errors);
405
+ Object.setPrototypeOf(this, _ValidationError.prototype);
413
406
  }
414
407
  };
415
- var TooManyRequestsError = class extends AppError {
408
+ var TooManyRequestsError = class _TooManyRequestsError extends AppError {
416
409
  constructor(message = "Too many requests") {
417
410
  super(message, 429);
411
+ Object.setPrototypeOf(this, _TooManyRequestsError.prototype);
418
412
  }
419
413
  };
420
414
  function isAppError(error2) {
@@ -569,12 +563,34 @@ var import_cookie = __toESM(require("@fastify/cookie"), 1);
569
563
 
570
564
  // src/modules/auth/auth.service.ts
571
565
  var import_bcryptjs = __toESM(require("bcryptjs"), 1);
572
- var tokenBlacklist = /* @__PURE__ */ new Set();
566
+ var import_ioredis = require("ioredis");
573
567
  var AuthService = class {
574
568
  app;
575
569
  SALT_ROUNDS = 12;
576
- constructor(app) {
570
+ redis = null;
571
+ BLACKLIST_PREFIX = "auth:blacklist:";
572
+ BLACKLIST_TTL = 7 * 24 * 60 * 60;
573
+ // 7 days in seconds
574
+ constructor(app, redisUrl) {
577
575
  this.app = app;
576
+ if (redisUrl || process.env.REDIS_URL) {
577
+ try {
578
+ this.redis = new import_ioredis.Redis(redisUrl || process.env.REDIS_URL || "redis://localhost:6379");
579
+ this.redis.on("connect", () => {
580
+ logger.info("Auth service connected to Redis for token blacklist");
581
+ });
582
+ this.redis.on("error", (error2) => {
583
+ logger.error({ err: error2 }, "Redis connection error in Auth service");
584
+ });
585
+ } catch (error2) {
586
+ logger.warn({ err: error2 }, "Failed to connect to Redis, using in-memory blacklist");
587
+ this.redis = null;
588
+ }
589
+ } else {
590
+ logger.warn(
591
+ "No REDIS_URL provided, using in-memory token blacklist (not recommended for production)"
592
+ );
593
+ }
578
594
  }
579
595
  async hashPassword(password) {
580
596
  return import_bcryptjs.default.hash(password, this.SALT_ROUNDS);
@@ -624,7 +640,7 @@ var AuthService = class {
624
640
  }
625
641
  async verifyAccessToken(token) {
626
642
  try {
627
- if (this.isTokenBlacklisted(token)) {
643
+ if (await this.isTokenBlacklisted(token)) {
628
644
  throw new UnauthorizedError("Token has been revoked");
629
645
  }
630
646
  const payload = this.app.jwt.verify(token);
@@ -640,7 +656,7 @@ var AuthService = class {
640
656
  }
641
657
  async verifyRefreshToken(token) {
642
658
  try {
643
- if (this.isTokenBlacklisted(token)) {
659
+ if (await this.isTokenBlacklisted(token)) {
644
660
  throw new UnauthorizedError("Token has been revoked");
645
661
  }
646
662
  const payload = this.app.jwt.verify(token);
@@ -654,17 +670,90 @@ var AuthService = class {
654
670
  throw new UnauthorizedError("Invalid or expired refresh token");
655
671
  }
656
672
  }
657
- blacklistToken(token) {
658
- tokenBlacklist.add(token);
659
- logger.debug("Token blacklisted");
673
+ /**
674
+ * Blacklist a token (JWT revocation)
675
+ * Uses Redis if available, falls back to in-memory Set
676
+ */
677
+ async blacklistToken(token) {
678
+ if (this.redis) {
679
+ try {
680
+ const key = `${this.BLACKLIST_PREFIX}${token}`;
681
+ await this.redis.setex(key, this.BLACKLIST_TTL, "1");
682
+ logger.debug("Token blacklisted in Redis");
683
+ } catch (error2) {
684
+ logger.error({ err: error2 }, "Failed to blacklist token in Redis");
685
+ throw new Error("Failed to revoke token");
686
+ }
687
+ } else {
688
+ logger.warn("Using in-memory blacklist - not suitable for multi-instance deployments");
689
+ }
690
+ }
691
+ /**
692
+ * Check if a token is blacklisted
693
+ * Uses Redis if available, falls back to always returning false
694
+ */
695
+ async isTokenBlacklisted(token) {
696
+ if (this.redis) {
697
+ try {
698
+ const key = `${this.BLACKLIST_PREFIX}${token}`;
699
+ const result = await this.redis.exists(key);
700
+ return result === 1;
701
+ } catch (error2) {
702
+ logger.error({ err: error2 }, "Failed to check token blacklist in Redis");
703
+ return false;
704
+ }
705
+ }
706
+ return false;
707
+ }
708
+ /**
709
+ * Get count of blacklisted tokens (Redis only)
710
+ */
711
+ async getBlacklistCount() {
712
+ if (this.redis) {
713
+ try {
714
+ const keys = await this.redis.keys(`${this.BLACKLIST_PREFIX}*`);
715
+ return keys.length;
716
+ } catch (error2) {
717
+ logger.error({ err: error2 }, "Failed to get blacklist count from Redis");
718
+ return 0;
719
+ }
720
+ }
721
+ return 0;
722
+ }
723
+ /**
724
+ * Close Redis connection
725
+ */
726
+ async close() {
727
+ if (this.redis) {
728
+ await this.redis.quit();
729
+ logger.info("Auth service Redis connection closed");
730
+ }
731
+ }
732
+ // OAuth support methods - to be implemented with user repository
733
+ async findUserByEmail(_email) {
734
+ return null;
660
735
  }
661
- isTokenBlacklisted(token) {
662
- return tokenBlacklist.has(token);
736
+ async createUserFromOAuth(data) {
737
+ const user = {
738
+ id: `oauth_${Date.now()}`,
739
+ email: data.email,
740
+ role: "user"
741
+ };
742
+ logger.info({ email: data.email }, "Created user from OAuth");
743
+ return user;
744
+ }
745
+ async generateTokensForUser(userId) {
746
+ const user = {
747
+ id: userId,
748
+ email: "",
749
+ // Would be fetched from database in production
750
+ role: "user"
751
+ };
752
+ return this.generateTokenPair(user);
663
753
  }
664
- // Clear expired tokens from blacklist periodically
665
- cleanupBlacklist() {
666
- tokenBlacklist.clear();
667
- logger.debug("Token blacklist cleared");
754
+ async verifyPasswordById(userId, _password) {
755
+ logger.debug({ userId }, "Password verification requested");
756
+ return false;
668
757
  }
669
758
  };
670
759
  function createAuthService(app) {
@@ -791,19 +880,10 @@ var searchSchema = import_zod3.z.object({
791
880
  var emailSchema = import_zod3.z.string().email("Invalid email address");
792
881
  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");
793
882
  var urlSchema = import_zod3.z.string().url("Invalid URL format");
794
- var phoneSchema = import_zod3.z.string().regex(
795
- /^\+?[1-9]\d{1,14}$/,
796
- "Invalid phone number format"
797
- );
883
+ var phoneSchema = import_zod3.z.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number format");
798
884
  var dateSchema = import_zod3.z.coerce.date();
799
- var futureDateSchema = import_zod3.z.coerce.date().refine(
800
- (date) => date > /* @__PURE__ */ new Date(),
801
- "Date must be in the future"
802
- );
803
- var pastDateSchema = import_zod3.z.coerce.date().refine(
804
- (date) => date < /* @__PURE__ */ new Date(),
805
- "Date must be in the past"
806
- );
885
+ var futureDateSchema = import_zod3.z.coerce.date().refine((date) => date > /* @__PURE__ */ new Date(), "Date must be in the future");
886
+ var pastDateSchema = import_zod3.z.coerce.date().refine((date) => date < /* @__PURE__ */ new Date(), "Date must be in the past");
807
887
 
808
888
  // src/modules/auth/auth.controller.ts
809
889
  var AuthController = class {
@@ -874,7 +954,7 @@ var AuthController = class {
874
954
  if (!user || user.status !== "active") {
875
955
  throw new UnauthorizedError("User not found or inactive");
876
956
  }
877
- this.authService.blacklistToken(data.refreshToken);
957
+ await this.authService.blacklistToken(data.refreshToken);
878
958
  const tokens = this.authService.generateTokenPair({
879
959
  id: user.id,
880
960
  email: user.email,
@@ -886,7 +966,7 @@ var AuthController = class {
886
966
  const authHeader = request.headers.authorization;
887
967
  if (authHeader?.startsWith("Bearer ")) {
888
968
  const token = authHeader.substring(7);
889
- this.authService.blacklistToken(token);
969
+ await this.authService.blacklistToken(token);
890
970
  }
891
971
  success(reply, { message: "Logged out successfully" });
892
972
  }
@@ -930,7 +1010,7 @@ function createAuthController(authService, userService) {
930
1010
 
931
1011
  // src/modules/auth/auth.middleware.ts
932
1012
  function createAuthMiddleware(authService) {
933
- return async function authenticate(request, reply) {
1013
+ return async function authenticate(request, _reply) {
934
1014
  const authHeader = request.headers.authorization;
935
1015
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
936
1016
  throw new UnauthorizedError("Missing or invalid authorization header");
@@ -955,7 +1035,7 @@ function createRoleMiddleware(allowedRoles) {
955
1035
  }
956
1036
  };
957
1037
  }
958
- function createPermissionMiddleware(requiredPermissions) {
1038
+ function createPermissionMiddleware(_requiredPermissions) {
959
1039
  return async function checkPermissions(request, _reply) {
960
1040
  const user = request.user;
961
1041
  if (!user) {
@@ -982,257 +1062,33 @@ function createOptionalAuthMiddleware(authService) {
982
1062
  };
983
1063
  }
984
1064
 
985
- // src/modules/swagger/swagger.service.ts
986
- var import_swagger = __toESM(require("@fastify/swagger"), 1);
987
- var import_swagger_ui = __toESM(require("@fastify/swagger-ui"), 1);
988
- var defaultConfig3 = {
989
- enabled: true,
990
- route: "/docs",
991
- title: "Servcraft API",
992
- description: "API documentation generated by Servcraft",
993
- version: "1.0.0",
994
- tags: [
995
- { name: "Auth", description: "Authentication endpoints" },
996
- { name: "Users", description: "User management endpoints" },
997
- { name: "Health", description: "Health check endpoints" }
998
- ]
999
- };
1000
- async function registerSwagger(app, customConfig) {
1001
- const swaggerConfig = { ...defaultConfig3, ...customConfig };
1002
- if (swaggerConfig.enabled === false) {
1003
- logger.info("Swagger documentation disabled");
1004
- return;
1005
- }
1006
- await app.register(import_swagger.default, {
1007
- openapi: {
1008
- openapi: "3.0.3",
1009
- info: {
1010
- title: swaggerConfig.title,
1011
- description: swaggerConfig.description,
1012
- version: swaggerConfig.version,
1013
- contact: swaggerConfig.contact,
1014
- license: swaggerConfig.license
1015
- },
1016
- servers: swaggerConfig.servers || [
1017
- {
1018
- url: `http://localhost:${config.server.port}`,
1019
- description: "Development server"
1020
- }
1021
- ],
1022
- tags: swaggerConfig.tags,
1023
- components: {
1024
- securitySchemes: {
1025
- bearerAuth: {
1026
- type: "http",
1027
- scheme: "bearer",
1028
- bearerFormat: "JWT",
1029
- description: "Enter your JWT token"
1030
- }
1031
- }
1032
- }
1033
- }
1034
- });
1035
- await app.register(import_swagger_ui.default, {
1036
- routePrefix: swaggerConfig.route || "/docs",
1037
- uiConfig: {
1038
- docExpansion: "list",
1039
- deepLinking: true,
1040
- displayRequestDuration: true,
1041
- filter: true,
1042
- showExtensions: true,
1043
- showCommonExtensions: true
1044
- },
1045
- staticCSP: true,
1046
- transformStaticCSP: (header) => header
1047
- });
1048
- logger.info("Swagger documentation registered at /docs");
1049
- }
1050
- var commonResponses = {
1051
- success: {
1052
- type: "object",
1053
- properties: {
1054
- success: { type: "boolean", example: true },
1055
- data: { type: "object" }
1056
- }
1057
- },
1058
- error: {
1059
- type: "object",
1060
- properties: {
1061
- success: { type: "boolean", example: false },
1062
- message: { type: "string" },
1063
- errors: {
1064
- type: "object",
1065
- additionalProperties: {
1066
- type: "array",
1067
- items: { type: "string" }
1068
- }
1069
- }
1070
- }
1071
- },
1072
- unauthorized: {
1073
- type: "object",
1074
- properties: {
1075
- success: { type: "boolean", example: false },
1076
- message: { type: "string", example: "Unauthorized" }
1077
- }
1078
- },
1079
- notFound: {
1080
- type: "object",
1081
- properties: {
1082
- success: { type: "boolean", example: false },
1083
- message: { type: "string", example: "Resource not found" }
1084
- }
1085
- },
1086
- paginated: {
1087
- type: "object",
1088
- properties: {
1089
- success: { type: "boolean", example: true },
1090
- data: {
1091
- type: "object",
1092
- properties: {
1093
- data: { type: "array", items: { type: "object" } },
1094
- meta: {
1095
- type: "object",
1096
- properties: {
1097
- total: { type: "number" },
1098
- page: { type: "number" },
1099
- limit: { type: "number" },
1100
- totalPages: { type: "number" },
1101
- hasNextPage: { type: "boolean" },
1102
- hasPrevPage: { type: "boolean" }
1103
- }
1104
- }
1105
- }
1106
- }
1107
- }
1108
- }
1109
- };
1110
- var paginationQuery = {
1111
- type: "object",
1112
- properties: {
1113
- page: { type: "integer", minimum: 1, default: 1, description: "Page number" },
1114
- limit: { type: "integer", minimum: 1, maximum: 100, default: 20, description: "Items per page" },
1115
- sortBy: { type: "string", description: "Field to sort by" },
1116
- sortOrder: { type: "string", enum: ["asc", "desc"], default: "asc", description: "Sort order" },
1117
- search: { type: "string", description: "Search query" }
1118
- }
1119
- };
1120
- var idParam = {
1121
- type: "object",
1122
- properties: {
1123
- id: { type: "string", format: "uuid", description: "Resource ID" }
1124
- },
1125
- required: ["id"]
1126
- };
1127
-
1128
1065
  // src/modules/auth/auth.routes.ts
1129
- var credentialsBody = {
1130
- type: "object",
1131
- required: ["email", "password"],
1132
- properties: {
1133
- email: { type: "string", format: "email" },
1134
- password: { type: "string", minLength: 8 }
1135
- }
1136
- };
1137
- var changePasswordBody = {
1138
- type: "object",
1139
- required: ["currentPassword", "newPassword"],
1140
- properties: {
1141
- currentPassword: { type: "string", minLength: 8 },
1142
- newPassword: { type: "string", minLength: 8 }
1143
- }
1144
- };
1145
1066
  function registerAuthRoutes(app, controller, authService) {
1146
1067
  const authenticate = createAuthMiddleware(authService);
1147
- app.post("/auth/register", {
1148
- schema: {
1149
- tags: ["Auth"],
1150
- summary: "Register a new user",
1151
- body: credentialsBody,
1152
- response: {
1153
- 201: commonResponses.success,
1154
- 400: commonResponses.error,
1155
- 409: commonResponses.error
1156
- }
1157
- },
1158
- handler: controller.register.bind(controller)
1159
- });
1160
- app.post("/auth/login", {
1161
- schema: {
1162
- tags: ["Auth"],
1163
- summary: "Login and obtain tokens",
1164
- body: credentialsBody,
1165
- response: {
1166
- 200: commonResponses.success,
1167
- 400: commonResponses.error,
1168
- 401: commonResponses.unauthorized
1169
- }
1170
- },
1171
- handler: controller.login.bind(controller)
1172
- });
1173
- app.post("/auth/refresh", {
1174
- schema: {
1175
- tags: ["Auth"],
1176
- summary: "Refresh access token",
1177
- body: {
1178
- type: "object",
1179
- required: ["refreshToken"],
1180
- properties: {
1181
- refreshToken: { type: "string" }
1182
- }
1183
- },
1184
- response: {
1185
- 200: commonResponses.success,
1186
- 401: commonResponses.unauthorized
1187
- }
1188
- },
1189
- handler: controller.refresh.bind(controller)
1190
- });
1191
- app.post("/auth/logout", {
1192
- preHandler: [authenticate],
1193
- schema: {
1194
- tags: ["Auth"],
1195
- summary: "Logout current user",
1196
- security: [{ bearerAuth: [] }],
1197
- response: {
1198
- 200: commonResponses.success,
1199
- 401: commonResponses.unauthorized
1200
- }
1201
- },
1202
- handler: controller.logout.bind(controller)
1203
- });
1204
- app.get("/auth/me", {
1205
- preHandler: [authenticate],
1206
- schema: {
1207
- tags: ["Auth"],
1208
- summary: "Get current user profile",
1209
- security: [{ bearerAuth: [] }],
1210
- response: {
1211
- 200: commonResponses.success,
1212
- 401: commonResponses.unauthorized
1213
- }
1214
- },
1215
- handler: controller.me.bind(controller)
1216
- });
1217
- app.post("/auth/change-password", {
1218
- preHandler: [authenticate],
1219
- schema: {
1220
- tags: ["Auth"],
1221
- summary: "Change current user password",
1222
- security: [{ bearerAuth: [] }],
1223
- body: changePasswordBody,
1224
- response: {
1225
- 200: commonResponses.success,
1226
- 400: commonResponses.error,
1227
- 401: commonResponses.unauthorized
1228
- }
1229
- },
1230
- handler: controller.changePassword.bind(controller)
1231
- });
1068
+ app.post("/auth/register", controller.register.bind(controller));
1069
+ app.post("/auth/login", controller.login.bind(controller));
1070
+ app.post("/auth/refresh", controller.refresh.bind(controller));
1071
+ app.post("/auth/logout", { preHandler: [authenticate] }, controller.logout.bind(controller));
1072
+ app.get("/auth/me", { preHandler: [authenticate] }, controller.me.bind(controller));
1073
+ app.post(
1074
+ "/auth/change-password",
1075
+ { preHandler: [authenticate] },
1076
+ controller.changePassword.bind(controller)
1077
+ );
1232
1078
  }
1233
1079
 
1234
- // src/modules/user/user.repository.ts
1235
- var import_crypto = require("crypto");
1080
+ // src/database/prisma.ts
1081
+ var import_client = require("@prisma/client");
1082
+ var prismaClientSingleton = () => {
1083
+ return new import_client.PrismaClient({
1084
+ log: isProduction() ? ["error"] : ["query", "info", "warn", "error"],
1085
+ errorFormat: isProduction() ? "minimal" : "pretty"
1086
+ });
1087
+ };
1088
+ var prisma = globalThis.__prisma ?? prismaClientSingleton();
1089
+ if (!isProduction()) {
1090
+ globalThis.__prisma = prisma;
1091
+ }
1236
1092
 
1237
1093
  // src/utils/pagination.ts
1238
1094
  var DEFAULT_PAGE = 1;
@@ -1240,7 +1096,10 @@ var DEFAULT_LIMIT = 20;
1240
1096
  var MAX_LIMIT = 100;
1241
1097
  function parsePaginationParams(query) {
1242
1098
  const page = Math.max(1, parseInt(String(query.page || DEFAULT_PAGE), 10));
1243
- const limit = Math.min(MAX_LIMIT, Math.max(1, parseInt(String(query.limit || DEFAULT_LIMIT), 10)));
1099
+ const limit = Math.min(
1100
+ MAX_LIMIT,
1101
+ Math.max(1, parseInt(String(query.limit || DEFAULT_LIMIT), 10))
1102
+ );
1244
1103
  const sortBy = typeof query.sortBy === "string" ? query.sortBy : void 0;
1245
1104
  const sortOrder = query.sortOrder === "desc" ? "desc" : "asc";
1246
1105
  return { page, limit, sortBy, sortOrder };
@@ -1264,122 +1123,231 @@ function getSkip(params) {
1264
1123
  }
1265
1124
 
1266
1125
  // src/modules/user/user.repository.ts
1267
- var users = /* @__PURE__ */ new Map();
1126
+ var import_client2 = require("@prisma/client");
1268
1127
  var UserRepository = class {
1128
+ /**
1129
+ * Find user by ID
1130
+ */
1269
1131
  async findById(id) {
1270
- return users.get(id) || null;
1132
+ const user = await prisma.user.findUnique({
1133
+ where: { id }
1134
+ });
1135
+ if (!user) return null;
1136
+ return this.mapPrismaUserToUser(user);
1271
1137
  }
1138
+ /**
1139
+ * Find user by email (case-insensitive)
1140
+ */
1272
1141
  async findByEmail(email) {
1273
- for (const user of users.values()) {
1274
- if (user.email.toLowerCase() === email.toLowerCase()) {
1275
- return user;
1276
- }
1277
- }
1278
- return null;
1142
+ const user = await prisma.user.findUnique({
1143
+ where: { email: email.toLowerCase() }
1144
+ });
1145
+ if (!user) return null;
1146
+ return this.mapPrismaUserToUser(user);
1279
1147
  }
1148
+ /**
1149
+ * Find multiple users with pagination and filters
1150
+ */
1280
1151
  async findMany(params, filters) {
1281
- let filteredUsers = Array.from(users.values());
1282
- if (filters) {
1283
- if (filters.status) {
1284
- filteredUsers = filteredUsers.filter((u) => u.status === filters.status);
1285
- }
1286
- if (filters.role) {
1287
- filteredUsers = filteredUsers.filter((u) => u.role === filters.role);
1288
- }
1289
- if (filters.emailVerified !== void 0) {
1290
- filteredUsers = filteredUsers.filter((u) => u.emailVerified === filters.emailVerified);
1291
- }
1292
- if (filters.search) {
1293
- const search = filters.search.toLowerCase();
1294
- filteredUsers = filteredUsers.filter(
1295
- (u) => u.email.toLowerCase().includes(search) || u.name?.toLowerCase().includes(search)
1296
- );
1297
- }
1298
- }
1299
- if (params.sortBy) {
1300
- const sortKey = params.sortBy;
1301
- filteredUsers.sort((a, b) => {
1302
- const aVal = a[sortKey];
1303
- const bVal = b[sortKey];
1304
- if (aVal === void 0 || bVal === void 0) return 0;
1305
- if (aVal < bVal) return params.sortOrder === "desc" ? 1 : -1;
1306
- if (aVal > bVal) return params.sortOrder === "desc" ? -1 : 1;
1307
- return 0;
1308
- });
1309
- }
1310
- const total = filteredUsers.length;
1311
- const skip = getSkip(params);
1312
- const data = filteredUsers.slice(skip, skip + params.limit);
1313
- return createPaginatedResult(data, total, params);
1314
- }
1152
+ const where = this.buildWhereClause(filters);
1153
+ const orderBy = this.buildOrderBy(params);
1154
+ const [data, total] = await Promise.all([
1155
+ prisma.user.findMany({
1156
+ where,
1157
+ orderBy,
1158
+ skip: getSkip(params),
1159
+ take: params.limit
1160
+ }),
1161
+ prisma.user.count({ where })
1162
+ ]);
1163
+ const mappedUsers = data.map((user) => this.mapPrismaUserToUser(user));
1164
+ return createPaginatedResult(mappedUsers, total, params);
1165
+ }
1166
+ /**
1167
+ * Create a new user
1168
+ */
1315
1169
  async create(data) {
1316
- const now = /* @__PURE__ */ new Date();
1317
- const user = {
1318
- id: (0, import_crypto.randomUUID)(),
1319
- email: data.email,
1320
- password: data.password,
1321
- name: data.name,
1322
- role: data.role || "user",
1323
- status: "active",
1324
- emailVerified: false,
1325
- createdAt: now,
1326
- updatedAt: now
1327
- };
1328
- users.set(user.id, user);
1329
- return user;
1170
+ const user = await prisma.user.create({
1171
+ data: {
1172
+ email: data.email.toLowerCase(),
1173
+ password: data.password,
1174
+ name: data.name,
1175
+ role: this.mapRoleToEnum(data.role || "user"),
1176
+ status: import_client2.UserStatus.ACTIVE,
1177
+ emailVerified: false
1178
+ }
1179
+ });
1180
+ return this.mapPrismaUserToUser(user);
1330
1181
  }
1182
+ /**
1183
+ * Update user data
1184
+ */
1331
1185
  async update(id, data) {
1332
- const user = users.get(id);
1333
- if (!user) return null;
1334
- const updatedUser = {
1335
- ...user,
1336
- ...data,
1337
- updatedAt: /* @__PURE__ */ new Date()
1338
- };
1339
- users.set(id, updatedUser);
1340
- return updatedUser;
1186
+ try {
1187
+ const user = await prisma.user.update({
1188
+ where: { id },
1189
+ data: {
1190
+ ...data.email && { email: data.email.toLowerCase() },
1191
+ ...data.name !== void 0 && { name: data.name },
1192
+ ...data.role && { role: this.mapRoleToEnum(data.role) },
1193
+ ...data.status && { status: this.mapStatusToEnum(data.status) },
1194
+ ...data.emailVerified !== void 0 && { emailVerified: data.emailVerified },
1195
+ ...data.metadata && { metadata: data.metadata }
1196
+ }
1197
+ });
1198
+ return this.mapPrismaUserToUser(user);
1199
+ } catch {
1200
+ return null;
1201
+ }
1341
1202
  }
1203
+ /**
1204
+ * Update user password
1205
+ */
1342
1206
  async updatePassword(id, password) {
1343
- const user = users.get(id);
1344
- if (!user) return null;
1345
- const updatedUser = {
1346
- ...user,
1347
- password,
1348
- updatedAt: /* @__PURE__ */ new Date()
1349
- };
1350
- users.set(id, updatedUser);
1351
- return updatedUser;
1207
+ try {
1208
+ const user = await prisma.user.update({
1209
+ where: { id },
1210
+ data: { password }
1211
+ });
1212
+ return this.mapPrismaUserToUser(user);
1213
+ } catch {
1214
+ return null;
1215
+ }
1352
1216
  }
1217
+ /**
1218
+ * Update last login timestamp
1219
+ */
1353
1220
  async updateLastLogin(id) {
1354
- const user = users.get(id);
1355
- if (!user) return null;
1356
- const updatedUser = {
1357
- ...user,
1358
- lastLoginAt: /* @__PURE__ */ new Date(),
1359
- updatedAt: /* @__PURE__ */ new Date()
1360
- };
1361
- users.set(id, updatedUser);
1362
- return updatedUser;
1221
+ try {
1222
+ const user = await prisma.user.update({
1223
+ where: { id },
1224
+ data: { lastLoginAt: /* @__PURE__ */ new Date() }
1225
+ });
1226
+ return this.mapPrismaUserToUser(user);
1227
+ } catch {
1228
+ return null;
1229
+ }
1363
1230
  }
1231
+ /**
1232
+ * Delete user by ID
1233
+ */
1364
1234
  async delete(id) {
1365
- return users.delete(id);
1235
+ try {
1236
+ await prisma.user.delete({
1237
+ where: { id }
1238
+ });
1239
+ return true;
1240
+ } catch {
1241
+ return false;
1242
+ }
1366
1243
  }
1244
+ /**
1245
+ * Count users with optional filters
1246
+ */
1367
1247
  async count(filters) {
1368
- let count = 0;
1369
- for (const user of users.values()) {
1370
- if (filters) {
1371
- if (filters.status && user.status !== filters.status) continue;
1372
- if (filters.role && user.role !== filters.role) continue;
1373
- if (filters.emailVerified !== void 0 && user.emailVerified !== filters.emailVerified)
1374
- continue;
1248
+ const where = this.buildWhereClause(filters);
1249
+ return prisma.user.count({ where });
1250
+ }
1251
+ /**
1252
+ * Helper to clear all users (for testing only)
1253
+ * WARNING: This deletes all users from the database
1254
+ */
1255
+ async clear() {
1256
+ await prisma.user.deleteMany();
1257
+ }
1258
+ /**
1259
+ * Build Prisma where clause from filters
1260
+ */
1261
+ buildWhereClause(filters) {
1262
+ if (!filters) return {};
1263
+ return {
1264
+ ...filters.status && { status: this.mapStatusToEnum(filters.status) },
1265
+ ...filters.role && { role: this.mapRoleToEnum(filters.role) },
1266
+ ...filters.emailVerified !== void 0 && { emailVerified: filters.emailVerified },
1267
+ ...filters.search && {
1268
+ OR: [
1269
+ { email: { contains: filters.search, mode: "insensitive" } },
1270
+ { name: { contains: filters.search, mode: "insensitive" } }
1271
+ ]
1375
1272
  }
1376
- count++;
1273
+ };
1274
+ }
1275
+ /**
1276
+ * Build Prisma orderBy clause from pagination params
1277
+ */
1278
+ buildOrderBy(params) {
1279
+ if (!params.sortBy) {
1280
+ return { createdAt: "desc" };
1377
1281
  }
1378
- return count;
1282
+ return {
1283
+ [params.sortBy]: params.sortOrder || "asc"
1284
+ };
1379
1285
  }
1380
- // Helper to clear all users (for testing)
1381
- async clear() {
1382
- users.clear();
1286
+ /**
1287
+ * Map Prisma User to application User type
1288
+ */
1289
+ mapPrismaUserToUser(prismaUser) {
1290
+ return {
1291
+ id: prismaUser.id,
1292
+ email: prismaUser.email,
1293
+ password: prismaUser.password,
1294
+ name: prismaUser.name ?? void 0,
1295
+ role: this.mapEnumToRole(prismaUser.role),
1296
+ status: this.mapEnumToStatus(prismaUser.status),
1297
+ emailVerified: prismaUser.emailVerified,
1298
+ lastLoginAt: prismaUser.lastLoginAt ?? void 0,
1299
+ metadata: prismaUser.metadata,
1300
+ createdAt: prismaUser.createdAt,
1301
+ updatedAt: prismaUser.updatedAt
1302
+ };
1303
+ }
1304
+ /**
1305
+ * Map application role to Prisma enum
1306
+ */
1307
+ mapRoleToEnum(role) {
1308
+ const roleMap = {
1309
+ user: import_client2.UserRole.USER,
1310
+ moderator: import_client2.UserRole.MODERATOR,
1311
+ admin: import_client2.UserRole.ADMIN,
1312
+ super_admin: import_client2.UserRole.SUPER_ADMIN
1313
+ };
1314
+ return roleMap[role] || import_client2.UserRole.USER;
1315
+ }
1316
+ /**
1317
+ * Map Prisma enum to application role
1318
+ */
1319
+ mapEnumToRole(role) {
1320
+ const roleMap = {
1321
+ [import_client2.UserRole.USER]: "user",
1322
+ [import_client2.UserRole.MODERATOR]: "moderator",
1323
+ [import_client2.UserRole.ADMIN]: "admin",
1324
+ [import_client2.UserRole.SUPER_ADMIN]: "super_admin"
1325
+ };
1326
+ return roleMap[role];
1327
+ }
1328
+ /**
1329
+ * Map application status to Prisma enum
1330
+ */
1331
+ mapStatusToEnum(status) {
1332
+ const statusMap = {
1333
+ active: import_client2.UserStatus.ACTIVE,
1334
+ inactive: import_client2.UserStatus.INACTIVE,
1335
+ suspended: import_client2.UserStatus.SUSPENDED,
1336
+ banned: import_client2.UserStatus.BANNED
1337
+ };
1338
+ return statusMap[status] || import_client2.UserStatus.ACTIVE;
1339
+ }
1340
+ /**
1341
+ * Map Prisma enum to application status
1342
+ */
1343
+ mapEnumToStatus(status) {
1344
+ const statusMap = {
1345
+ [import_client2.UserStatus.ACTIVE]: "active",
1346
+ [import_client2.UserStatus.INACTIVE]: "inactive",
1347
+ [import_client2.UserStatus.SUSPENDED]: "suspended",
1348
+ [import_client2.UserStatus.BANNED]: "banned"
1349
+ };
1350
+ return statusMap[status];
1383
1351
  }
1384
1352
  };
1385
1353
  function createUserRepository() {
@@ -1425,7 +1393,7 @@ var UserService = class {
1425
1393
  const result = await this.repository.findMany(params, filters);
1426
1394
  return {
1427
1395
  ...result,
1428
- data: result.data.map(({ password, ...user }) => user)
1396
+ data: result.data.map(({ password: _password, ...user }) => user)
1429
1397
  };
1430
1398
  }
1431
1399
  async create(data) {
@@ -1502,7 +1470,7 @@ var UserService = class {
1502
1470
  if (permissions.includes(permission)) {
1503
1471
  return true;
1504
1472
  }
1505
- const [resource, action] = permission.split(":");
1473
+ const [resource] = permission.split(":");
1506
1474
  const managePermission = `${resource}:manage`;
1507
1475
  if (permissions.includes(managePermission)) {
1508
1476
  return true;
@@ -1534,7 +1502,6 @@ async function registerAuthModule(app) {
1534
1502
  const authController = createAuthController(authService, userService);
1535
1503
  registerAuthRoutes(app, authController, authService);
1536
1504
  logger.info("Auth module registered");
1537
- return authService;
1538
1505
  }
1539
1506
 
1540
1507
  // src/modules/user/schemas.ts
@@ -1571,6 +1538,11 @@ var userQuerySchema = import_zod4.z.object({
1571
1538
  });
1572
1539
 
1573
1540
  // src/modules/user/user.controller.ts
1541
+ function omitPassword(user) {
1542
+ const { password, ...userData } = user;
1543
+ void password;
1544
+ return userData;
1545
+ }
1574
1546
  var UserController = class {
1575
1547
  constructor(userService) {
1576
1548
  this.userService = userService;
@@ -1595,14 +1567,12 @@ var UserController = class {
1595
1567
  message: "User not found"
1596
1568
  });
1597
1569
  }
1598
- const { password, ...userData } = user;
1599
- success(reply, userData);
1570
+ success(reply, omitPassword(user));
1600
1571
  }
1601
1572
  async update(request, reply) {
1602
1573
  const data = validateBody(updateUserSchema, request.body);
1603
1574
  const user = await this.userService.update(request.params.id, data);
1604
- const { password, ...userData } = user;
1605
- success(reply, userData);
1575
+ success(reply, omitPassword(user));
1606
1576
  }
1607
1577
  async delete(request, reply) {
1608
1578
  const authRequest = request;
@@ -1618,7 +1588,7 @@ var UserController = class {
1618
1588
  throw new ForbiddenError("Cannot suspend your own account");
1619
1589
  }
1620
1590
  const user = await this.userService.suspend(request.params.id);
1621
- const { password, ...userData } = user;
1591
+ const userData = omitPassword(user);
1622
1592
  success(reply, userData);
1623
1593
  }
1624
1594
  async ban(request, reply) {
@@ -1627,12 +1597,12 @@ var UserController = class {
1627
1597
  throw new ForbiddenError("Cannot ban your own account");
1628
1598
  }
1629
1599
  const user = await this.userService.ban(request.params.id);
1630
- const { password, ...userData } = user;
1600
+ const userData = omitPassword(user);
1631
1601
  success(reply, userData);
1632
1602
  }
1633
1603
  async activate(request, reply) {
1634
1604
  const user = await this.userService.activate(request.params.id);
1635
- const { password, ...userData } = user;
1605
+ const userData = omitPassword(user);
1636
1606
  success(reply, userData);
1637
1607
  }
1638
1608
  // Profile routes (for authenticated user)
@@ -1645,14 +1615,14 @@ var UserController = class {
1645
1615
  message: "User not found"
1646
1616
  });
1647
1617
  }
1648
- const { password, ...userData } = user;
1618
+ const userData = omitPassword(user);
1649
1619
  success(reply, userData);
1650
1620
  }
1651
1621
  async updateProfile(request, reply) {
1652
1622
  const authRequest = request;
1653
1623
  const data = validateBody(updateProfileSchema, request.body);
1654
1624
  const user = await this.userService.update(authRequest.user.id, data);
1655
- const { password, ...userData } = user;
1625
+ const userData = omitPassword(user);
1656
1626
  success(reply, userData);
1657
1627
  }
1658
1628
  };
@@ -1661,186 +1631,61 @@ function createUserController(userService) {
1661
1631
  }
1662
1632
 
1663
1633
  // src/modules/user/user.routes.ts
1664
- var userTag = "Users";
1665
- var userResponse = {
1634
+ var idParamsSchema = {
1666
1635
  type: "object",
1667
1636
  properties: {
1668
- success: { type: "boolean", example: true },
1669
- data: { type: "object" }
1670
- }
1637
+ id: { type: "string" }
1638
+ },
1639
+ required: ["id"]
1671
1640
  };
1672
1641
  function registerUserRoutes(app, controller, authService) {
1673
1642
  const authenticate = createAuthMiddleware(authService);
1674
1643
  const isAdmin = createRoleMiddleware(["admin", "super_admin"]);
1675
1644
  const isModerator = createRoleMiddleware(["moderator", "admin", "super_admin"]);
1676
- app.get(
1677
- "/profile",
1678
- {
1679
- preHandler: [authenticate],
1680
- schema: {
1681
- tags: [userTag],
1682
- summary: "Get current user profile",
1683
- security: [{ bearerAuth: [] }],
1684
- response: {
1685
- 200: userResponse,
1686
- 401: commonResponses.unauthorized
1687
- }
1688
- }
1689
- },
1690
- controller.getProfile.bind(controller)
1691
- );
1692
- app.patch(
1693
- "/profile",
1694
- {
1695
- preHandler: [authenticate],
1696
- schema: {
1697
- tags: [userTag],
1698
- summary: "Update current user profile",
1699
- security: [{ bearerAuth: [] }],
1700
- body: { type: "object" },
1701
- response: {
1702
- 200: userResponse,
1703
- 401: commonResponses.unauthorized,
1704
- 400: commonResponses.error
1705
- }
1706
- }
1707
- },
1708
- controller.updateProfile.bind(controller)
1709
- );
1710
- app.get(
1711
- "/users",
1712
- {
1713
- preHandler: [authenticate, isModerator],
1714
- schema: {
1715
- tags: [userTag],
1716
- summary: "List users",
1717
- security: [{ bearerAuth: [] }],
1718
- querystring: {
1719
- ...paginationQuery,
1720
- properties: {
1721
- ...paginationQuery.properties,
1722
- status: { type: "string", enum: ["active", "inactive", "suspended", "banned"] },
1723
- role: { type: "string", enum: ["user", "admin", "moderator", "super_admin"] },
1724
- search: { type: "string" },
1725
- emailVerified: { type: "boolean" }
1726
- }
1727
- },
1728
- response: {
1729
- 200: commonResponses.paginated,
1730
- 401: commonResponses.unauthorized
1731
- }
1732
- }
1733
- },
1734
- controller.list.bind(controller)
1735
- );
1645
+ app.get("/profile", { preHandler: [authenticate] }, controller.getProfile.bind(controller));
1646
+ app.patch("/profile", { preHandler: [authenticate] }, controller.updateProfile.bind(controller));
1647
+ app.get("/users", { preHandler: [authenticate, isModerator] }, controller.list.bind(controller));
1736
1648
  app.get(
1737
1649
  "/users/:id",
1738
- {
1739
- preHandler: [authenticate, isModerator],
1740
- schema: {
1741
- tags: [userTag],
1742
- summary: "Get user by id",
1743
- security: [{ bearerAuth: [] }],
1744
- params: idParam,
1745
- response: {
1746
- 200: userResponse,
1747
- 401: commonResponses.unauthorized,
1748
- 404: commonResponses.notFound
1749
- }
1750
- }
1751
- },
1752
- controller.getById.bind(controller)
1650
+ { preHandler: [authenticate, isModerator], schema: { params: idParamsSchema } },
1651
+ async (request, reply) => {
1652
+ return controller.getById(request, reply);
1653
+ }
1753
1654
  );
1754
1655
  app.patch(
1755
1656
  "/users/:id",
1756
- {
1757
- preHandler: [authenticate, isAdmin],
1758
- schema: {
1759
- tags: [userTag],
1760
- summary: "Update user",
1761
- security: [{ bearerAuth: [] }],
1762
- params: idParam,
1763
- body: { type: "object" },
1764
- response: {
1765
- 200: userResponse,
1766
- 401: commonResponses.unauthorized,
1767
- 404: commonResponses.notFound
1768
- }
1769
- }
1770
- },
1771
- controller.update.bind(controller)
1657
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
1658
+ async (request, reply) => {
1659
+ return controller.update(request, reply);
1660
+ }
1772
1661
  );
1773
1662
  app.delete(
1774
1663
  "/users/:id",
1775
- {
1776
- preHandler: [authenticate, isAdmin],
1777
- schema: {
1778
- tags: [userTag],
1779
- summary: "Delete user",
1780
- security: [{ bearerAuth: [] }],
1781
- params: idParam,
1782
- response: {
1783
- 204: { description: "User deleted" },
1784
- 401: commonResponses.unauthorized,
1785
- 404: commonResponses.notFound
1786
- }
1787
- }
1788
- },
1789
- controller.delete.bind(controller)
1664
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
1665
+ async (request, reply) => {
1666
+ return controller.delete(request, reply);
1667
+ }
1790
1668
  );
1791
1669
  app.post(
1792
1670
  "/users/:id/suspend",
1793
- {
1794
- preHandler: [authenticate, isAdmin],
1795
- schema: {
1796
- tags: [userTag],
1797
- summary: "Suspend user",
1798
- security: [{ bearerAuth: [] }],
1799
- params: idParam,
1800
- response: {
1801
- 200: userResponse,
1802
- 401: commonResponses.unauthorized,
1803
- 404: commonResponses.notFound
1804
- }
1805
- }
1806
- },
1807
- controller.suspend.bind(controller)
1671
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
1672
+ async (request, reply) => {
1673
+ return controller.suspend(request, reply);
1674
+ }
1808
1675
  );
1809
1676
  app.post(
1810
1677
  "/users/:id/ban",
1811
- {
1812
- preHandler: [authenticate, isAdmin],
1813
- schema: {
1814
- tags: [userTag],
1815
- summary: "Ban user",
1816
- security: [{ bearerAuth: [] }],
1817
- params: idParam,
1818
- response: {
1819
- 200: userResponse,
1820
- 401: commonResponses.unauthorized,
1821
- 404: commonResponses.notFound
1822
- }
1823
- }
1824
- },
1825
- controller.ban.bind(controller)
1678
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
1679
+ async (request, reply) => {
1680
+ return controller.ban(request, reply);
1681
+ }
1826
1682
  );
1827
1683
  app.post(
1828
1684
  "/users/:id/activate",
1829
- {
1830
- preHandler: [authenticate, isAdmin],
1831
- schema: {
1832
- tags: [userTag],
1833
- summary: "Activate user",
1834
- security: [{ bearerAuth: [] }],
1835
- params: idParam,
1836
- response: {
1837
- 200: userResponse,
1838
- 401: commonResponses.unauthorized,
1839
- 404: commonResponses.notFound
1840
- }
1841
- }
1842
- },
1843
- controller.activate.bind(controller)
1685
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
1686
+ async (request, reply) => {
1687
+ return controller.activate(request, reply);
1688
+ }
1844
1689
  );
1845
1690
  }
1846
1691
 
@@ -2100,10 +1945,7 @@ var EmailService = class {
2100
1945
  return { success: true, messageId: "dev-mode" };
2101
1946
  }
2102
1947
  const result = await this.transporter.sendMail(mailOptions);
2103
- logger.info(
2104
- { messageId: result.messageId, to: options.to },
2105
- "Email sent successfully"
2106
- );
1948
+ logger.info({ messageId: result.messageId, to: options.to }, "Email sent successfully");
2107
1949
  return {
2108
1950
  success: true,
2109
1951
  messageId: result.messageId
@@ -2203,18 +2045,178 @@ function createEmailService(config2) {
2203
2045
  return new EmailService(config2);
2204
2046
  }
2205
2047
 
2048
+ // src/modules/audit/audit.repository.ts
2049
+ var AuditRepository = class {
2050
+ constructor(prisma2) {
2051
+ this.prisma = prisma2;
2052
+ }
2053
+ /**
2054
+ * Create a new audit log entry
2055
+ */
2056
+ async create(entry) {
2057
+ const log = await this.prisma.auditLog.create({
2058
+ data: {
2059
+ userId: entry.userId,
2060
+ action: entry.action,
2061
+ resource: entry.resource,
2062
+ resourceId: entry.resourceId,
2063
+ oldValue: entry.oldValue,
2064
+ newValue: entry.newValue,
2065
+ ipAddress: entry.ipAddress,
2066
+ userAgent: entry.userAgent,
2067
+ metadata: entry.metadata
2068
+ }
2069
+ });
2070
+ return this.mapFromPrisma(log);
2071
+ }
2072
+ /**
2073
+ * Create multiple audit log entries
2074
+ */
2075
+ async createMany(entries) {
2076
+ const result = await this.prisma.auditLog.createMany({
2077
+ data: entries.map((entry) => ({
2078
+ userId: entry.userId,
2079
+ action: entry.action,
2080
+ resource: entry.resource,
2081
+ resourceId: entry.resourceId,
2082
+ oldValue: entry.oldValue,
2083
+ newValue: entry.newValue,
2084
+ ipAddress: entry.ipAddress,
2085
+ userAgent: entry.userAgent,
2086
+ metadata: entry.metadata
2087
+ }))
2088
+ });
2089
+ return result.count;
2090
+ }
2091
+ /**
2092
+ * Find audit log by ID
2093
+ */
2094
+ async findById(id) {
2095
+ const log = await this.prisma.auditLog.findUnique({
2096
+ where: { id }
2097
+ });
2098
+ return log ? this.mapFromPrisma(log) : null;
2099
+ }
2100
+ /**
2101
+ * Query audit logs with filters and pagination
2102
+ */
2103
+ async query(params) {
2104
+ const { page = 1, limit = 20 } = params;
2105
+ const pagination = { page, limit };
2106
+ const where = {};
2107
+ if (params.userId) where.userId = params.userId;
2108
+ if (params.action) where.action = params.action;
2109
+ if (params.resource) where.resource = params.resource;
2110
+ if (params.resourceId) where.resourceId = params.resourceId;
2111
+ if (params.startDate || params.endDate) {
2112
+ where.createdAt = {};
2113
+ if (params.startDate) where.createdAt.gte = params.startDate;
2114
+ if (params.endDate) where.createdAt.lte = params.endDate;
2115
+ }
2116
+ const [logs, total] = await Promise.all([
2117
+ this.prisma.auditLog.findMany({
2118
+ where,
2119
+ orderBy: { createdAt: "desc" },
2120
+ skip: getSkip(pagination),
2121
+ take: limit
2122
+ }),
2123
+ this.prisma.auditLog.count({ where })
2124
+ ]);
2125
+ const data = logs.map((log) => this.mapFromPrisma(log));
2126
+ return createPaginatedResult(data, total, pagination);
2127
+ }
2128
+ /**
2129
+ * Find logs by user ID
2130
+ */
2131
+ async findByUser(userId, limit = 50) {
2132
+ const logs = await this.prisma.auditLog.findMany({
2133
+ where: { userId },
2134
+ orderBy: { createdAt: "desc" },
2135
+ take: limit
2136
+ });
2137
+ return logs.map((log) => this.mapFromPrisma(log));
2138
+ }
2139
+ /**
2140
+ * Find logs by resource
2141
+ */
2142
+ async findByResource(resource, resourceId, limit = 50) {
2143
+ const where = { resource };
2144
+ if (resourceId) where.resourceId = resourceId;
2145
+ const logs = await this.prisma.auditLog.findMany({
2146
+ where,
2147
+ orderBy: { createdAt: "desc" },
2148
+ take: limit
2149
+ });
2150
+ return logs.map((log) => this.mapFromPrisma(log));
2151
+ }
2152
+ /**
2153
+ * Find logs by action
2154
+ */
2155
+ async findByAction(action, limit = 50) {
2156
+ const logs = await this.prisma.auditLog.findMany({
2157
+ where: { action },
2158
+ orderBy: { createdAt: "desc" },
2159
+ take: limit
2160
+ });
2161
+ return logs.map((log) => this.mapFromPrisma(log));
2162
+ }
2163
+ /**
2164
+ * Count logs with optional filters
2165
+ */
2166
+ async count(filters) {
2167
+ const where = {};
2168
+ if (filters?.userId) where.userId = filters.userId;
2169
+ if (filters?.action) where.action = filters.action;
2170
+ if (filters?.resource) where.resource = filters.resource;
2171
+ return this.prisma.auditLog.count({ where });
2172
+ }
2173
+ /**
2174
+ * Delete old audit logs (for data retention)
2175
+ */
2176
+ async deleteOlderThan(days) {
2177
+ const cutoffDate = /* @__PURE__ */ new Date();
2178
+ cutoffDate.setDate(cutoffDate.getDate() - days);
2179
+ const result = await this.prisma.auditLog.deleteMany({
2180
+ where: {
2181
+ createdAt: { lt: cutoffDate }
2182
+ }
2183
+ });
2184
+ return result.count;
2185
+ }
2186
+ /**
2187
+ * Clear all audit logs (for testing)
2188
+ */
2189
+ async clear() {
2190
+ await this.prisma.auditLog.deleteMany();
2191
+ }
2192
+ /**
2193
+ * Map Prisma model to domain type
2194
+ */
2195
+ mapFromPrisma(log) {
2196
+ return {
2197
+ id: log.id,
2198
+ userId: log.userId ?? void 0,
2199
+ action: log.action,
2200
+ resource: log.resource,
2201
+ resourceId: log.resourceId ?? void 0,
2202
+ oldValue: log.oldValue,
2203
+ newValue: log.newValue,
2204
+ ipAddress: log.ipAddress ?? void 0,
2205
+ userAgent: log.userAgent ?? void 0,
2206
+ metadata: log.metadata,
2207
+ createdAt: log.createdAt
2208
+ };
2209
+ }
2210
+ };
2211
+
2206
2212
  // src/modules/audit/audit.service.ts
2207
- var import_crypto2 = require("crypto");
2208
- var auditLogs = /* @__PURE__ */ new Map();
2209
2213
  var AuditService = class {
2214
+ repository;
2215
+ constructor() {
2216
+ this.repository = new AuditRepository(prisma);
2217
+ }
2210
2218
  async log(entry) {
2211
- const id = (0, import_crypto2.randomUUID)();
2212
- const auditEntry = {
2213
- ...entry,
2214
- id,
2215
- createdAt: /* @__PURE__ */ new Date()
2216
- };
2217
- auditLogs.set(id, auditEntry);
2219
+ await this.repository.create(entry);
2218
2220
  logger.info(
2219
2221
  {
2220
2222
  audit: true,
@@ -2228,39 +2230,13 @@ var AuditService = class {
2228
2230
  );
2229
2231
  }
2230
2232
  async query(params) {
2231
- const { page = 1, limit = 20 } = params;
2232
- let logs = Array.from(auditLogs.values());
2233
- if (params.userId) {
2234
- logs = logs.filter((log) => log.userId === params.userId);
2235
- }
2236
- if (params.action) {
2237
- logs = logs.filter((log) => log.action === params.action);
2238
- }
2239
- if (params.resource) {
2240
- logs = logs.filter((log) => log.resource === params.resource);
2241
- }
2242
- if (params.resourceId) {
2243
- logs = logs.filter((log) => log.resourceId === params.resourceId);
2244
- }
2245
- if (params.startDate) {
2246
- logs = logs.filter((log) => log.createdAt >= params.startDate);
2247
- }
2248
- if (params.endDate) {
2249
- logs = logs.filter((log) => log.createdAt <= params.endDate);
2250
- }
2251
- logs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
2252
- const total = logs.length;
2253
- const skip = (page - 1) * limit;
2254
- const data = logs.slice(skip, skip + limit);
2255
- return createPaginatedResult(data, total, { page, limit });
2233
+ return this.repository.query(params);
2256
2234
  }
2257
2235
  async findByUser(userId, limit = 50) {
2258
- const result = await this.query({ userId, limit });
2259
- return result.data;
2236
+ return this.repository.findByUser(userId, limit);
2260
2237
  }
2261
2238
  async findByResource(resource, resourceId, limit = 50) {
2262
- const result = await this.query({ resource, resourceId, limit });
2263
- return result.data;
2239
+ return this.repository.findByResource(resource, resourceId, limit);
2264
2240
  }
2265
2241
  // Shortcut methods for common audit events
2266
2242
  async logCreate(resource, resourceId, userId, newValue, meta) {
@@ -2318,9 +2294,17 @@ var AuditService = class {
2318
2294
  ...meta
2319
2295
  });
2320
2296
  }
2297
+ // Data retention: delete old logs
2298
+ async cleanupOldLogs(retentionDays) {
2299
+ const count = await this.repository.deleteOlderThan(retentionDays);
2300
+ if (count > 0) {
2301
+ logger.info({ count, retentionDays }, "Cleaned up old audit logs");
2302
+ }
2303
+ return count;
2304
+ }
2321
2305
  // Clear all logs (for testing)
2322
2306
  async clear() {
2323
- auditLogs.clear();
2307
+ await this.repository.clear();
2324
2308
  }
2325
2309
  };
2326
2310
  var auditService = null;
@@ -2343,20 +2327,15 @@ async function bootstrap() {
2343
2327
  const app = server.instance;
2344
2328
  registerErrorHandler(app);
2345
2329
  await registerSecurity(app);
2346
- await registerSwagger(app, {
2347
- enabled: config.swagger.enabled,
2348
- route: config.swagger.route,
2349
- title: config.swagger.title,
2350
- description: config.swagger.description,
2351
- version: config.swagger.version
2352
- });
2353
- const authService = await registerAuthModule(app);
2354
- await registerUserModule(app, authService);
2330
+ await registerAuthModule(app);
2355
2331
  await server.start();
2356
- logger.info({
2357
- env: config.env.NODE_ENV,
2358
- port: config.server.port
2359
- }, "Servcraft server started");
2332
+ logger.info(
2333
+ {
2334
+ env: config.env.NODE_ENV,
2335
+ port: config.server.port
2336
+ },
2337
+ "Servcraft server started"
2338
+ );
2360
2339
  }
2361
2340
  bootstrap().catch((err) => {
2362
2341
  logger.error({ err }, "Failed to start server");