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.js CHANGED
@@ -161,12 +161,6 @@ var envSchema = z.object({
161
161
  SMTP_FROM: z.string().optional(),
162
162
  // Redis (optional)
163
163
  REDIS_URL: z.string().optional(),
164
- // Swagger/OpenAPI
165
- SWAGGER_ENABLED: z.union([z.literal("true"), z.literal("false")]).default("true").transform((val) => val === "true"),
166
- SWAGGER_ROUTE: z.string().default("/docs"),
167
- SWAGGER_TITLE: z.string().default("Servcraft API"),
168
- SWAGGER_DESCRIPTION: z.string().default("API documentation"),
169
- SWAGGER_VERSION: z.string().default("1.0.0"),
170
164
  // Logging
171
165
  LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info")
172
166
  });
@@ -231,13 +225,6 @@ function createConfig() {
231
225
  },
232
226
  redis: {
233
227
  url: env.REDIS_URL
234
- },
235
- swagger: {
236
- enabled: env.SWAGGER_ENABLED,
237
- route: env.SWAGGER_ROUTE,
238
- title: env.SWAGGER_TITLE,
239
- description: env.SWAGGER_DESCRIPTION,
240
- version: env.SWAGGER_VERSION
241
228
  }
242
229
  };
243
230
  }
@@ -257,39 +244,46 @@ var AppError = class _AppError extends Error {
257
244
  Error.captureStackTrace(this, this.constructor);
258
245
  }
259
246
  };
260
- var NotFoundError = class extends AppError {
247
+ var NotFoundError = class _NotFoundError extends AppError {
261
248
  constructor(resource = "Resource") {
262
249
  super(`${resource} not found`, 404);
250
+ Object.setPrototypeOf(this, _NotFoundError.prototype);
263
251
  }
264
252
  };
265
- var UnauthorizedError = class extends AppError {
253
+ var UnauthorizedError = class _UnauthorizedError extends AppError {
266
254
  constructor(message = "Unauthorized") {
267
255
  super(message, 401);
256
+ Object.setPrototypeOf(this, _UnauthorizedError.prototype);
268
257
  }
269
258
  };
270
- var ForbiddenError = class extends AppError {
259
+ var ForbiddenError = class _ForbiddenError extends AppError {
271
260
  constructor(message = "Forbidden") {
272
261
  super(message, 403);
262
+ Object.setPrototypeOf(this, _ForbiddenError.prototype);
273
263
  }
274
264
  };
275
- var BadRequestError = class extends AppError {
265
+ var BadRequestError = class _BadRequestError extends AppError {
276
266
  constructor(message = "Bad request", errors) {
277
267
  super(message, 400, true, errors);
268
+ Object.setPrototypeOf(this, _BadRequestError.prototype);
278
269
  }
279
270
  };
280
- var ConflictError = class extends AppError {
271
+ var ConflictError = class _ConflictError extends AppError {
281
272
  constructor(message = "Resource already exists") {
282
273
  super(message, 409);
274
+ Object.setPrototypeOf(this, _ConflictError.prototype);
283
275
  }
284
276
  };
285
- var ValidationError = class extends AppError {
277
+ var ValidationError = class _ValidationError extends AppError {
286
278
  constructor(errors) {
287
279
  super("Validation failed", 422, true, errors);
280
+ Object.setPrototypeOf(this, _ValidationError.prototype);
288
281
  }
289
282
  };
290
- var TooManyRequestsError = class extends AppError {
283
+ var TooManyRequestsError = class _TooManyRequestsError extends AppError {
291
284
  constructor(message = "Too many requests") {
292
285
  super(message, 429);
286
+ Object.setPrototypeOf(this, _TooManyRequestsError.prototype);
293
287
  }
294
288
  };
295
289
  function isAppError(error2) {
@@ -444,12 +438,34 @@ import cookie from "@fastify/cookie";
444
438
 
445
439
  // src/modules/auth/auth.service.ts
446
440
  import bcrypt from "bcryptjs";
447
- var tokenBlacklist = /* @__PURE__ */ new Set();
441
+ import { Redis } from "ioredis";
448
442
  var AuthService = class {
449
443
  app;
450
444
  SALT_ROUNDS = 12;
451
- constructor(app) {
445
+ redis = null;
446
+ BLACKLIST_PREFIX = "auth:blacklist:";
447
+ BLACKLIST_TTL = 7 * 24 * 60 * 60;
448
+ // 7 days in seconds
449
+ constructor(app, redisUrl) {
452
450
  this.app = app;
451
+ if (redisUrl || process.env.REDIS_URL) {
452
+ try {
453
+ this.redis = new Redis(redisUrl || process.env.REDIS_URL || "redis://localhost:6379");
454
+ this.redis.on("connect", () => {
455
+ logger.info("Auth service connected to Redis for token blacklist");
456
+ });
457
+ this.redis.on("error", (error2) => {
458
+ logger.error({ err: error2 }, "Redis connection error in Auth service");
459
+ });
460
+ } catch (error2) {
461
+ logger.warn({ err: error2 }, "Failed to connect to Redis, using in-memory blacklist");
462
+ this.redis = null;
463
+ }
464
+ } else {
465
+ logger.warn(
466
+ "No REDIS_URL provided, using in-memory token blacklist (not recommended for production)"
467
+ );
468
+ }
453
469
  }
454
470
  async hashPassword(password) {
455
471
  return bcrypt.hash(password, this.SALT_ROUNDS);
@@ -499,7 +515,7 @@ var AuthService = class {
499
515
  }
500
516
  async verifyAccessToken(token) {
501
517
  try {
502
- if (this.isTokenBlacklisted(token)) {
518
+ if (await this.isTokenBlacklisted(token)) {
503
519
  throw new UnauthorizedError("Token has been revoked");
504
520
  }
505
521
  const payload = this.app.jwt.verify(token);
@@ -515,7 +531,7 @@ var AuthService = class {
515
531
  }
516
532
  async verifyRefreshToken(token) {
517
533
  try {
518
- if (this.isTokenBlacklisted(token)) {
534
+ if (await this.isTokenBlacklisted(token)) {
519
535
  throw new UnauthorizedError("Token has been revoked");
520
536
  }
521
537
  const payload = this.app.jwt.verify(token);
@@ -529,17 +545,90 @@ var AuthService = class {
529
545
  throw new UnauthorizedError("Invalid or expired refresh token");
530
546
  }
531
547
  }
532
- blacklistToken(token) {
533
- tokenBlacklist.add(token);
534
- logger.debug("Token blacklisted");
548
+ /**
549
+ * Blacklist a token (JWT revocation)
550
+ * Uses Redis if available, falls back to in-memory Set
551
+ */
552
+ async blacklistToken(token) {
553
+ if (this.redis) {
554
+ try {
555
+ const key = `${this.BLACKLIST_PREFIX}${token}`;
556
+ await this.redis.setex(key, this.BLACKLIST_TTL, "1");
557
+ logger.debug("Token blacklisted in Redis");
558
+ } catch (error2) {
559
+ logger.error({ err: error2 }, "Failed to blacklist token in Redis");
560
+ throw new Error("Failed to revoke token");
561
+ }
562
+ } else {
563
+ logger.warn("Using in-memory blacklist - not suitable for multi-instance deployments");
564
+ }
565
+ }
566
+ /**
567
+ * Check if a token is blacklisted
568
+ * Uses Redis if available, falls back to always returning false
569
+ */
570
+ async isTokenBlacklisted(token) {
571
+ if (this.redis) {
572
+ try {
573
+ const key = `${this.BLACKLIST_PREFIX}${token}`;
574
+ const result = await this.redis.exists(key);
575
+ return result === 1;
576
+ } catch (error2) {
577
+ logger.error({ err: error2 }, "Failed to check token blacklist in Redis");
578
+ return false;
579
+ }
580
+ }
581
+ return false;
582
+ }
583
+ /**
584
+ * Get count of blacklisted tokens (Redis only)
585
+ */
586
+ async getBlacklistCount() {
587
+ if (this.redis) {
588
+ try {
589
+ const keys = await this.redis.keys(`${this.BLACKLIST_PREFIX}*`);
590
+ return keys.length;
591
+ } catch (error2) {
592
+ logger.error({ err: error2 }, "Failed to get blacklist count from Redis");
593
+ return 0;
594
+ }
595
+ }
596
+ return 0;
597
+ }
598
+ /**
599
+ * Close Redis connection
600
+ */
601
+ async close() {
602
+ if (this.redis) {
603
+ await this.redis.quit();
604
+ logger.info("Auth service Redis connection closed");
605
+ }
606
+ }
607
+ // OAuth support methods - to be implemented with user repository
608
+ async findUserByEmail(_email) {
609
+ return null;
535
610
  }
536
- isTokenBlacklisted(token) {
537
- return tokenBlacklist.has(token);
611
+ async createUserFromOAuth(data) {
612
+ const user = {
613
+ id: `oauth_${Date.now()}`,
614
+ email: data.email,
615
+ role: "user"
616
+ };
617
+ logger.info({ email: data.email }, "Created user from OAuth");
618
+ return user;
619
+ }
620
+ async generateTokensForUser(userId) {
621
+ const user = {
622
+ id: userId,
623
+ email: "",
624
+ // Would be fetched from database in production
625
+ role: "user"
626
+ };
627
+ return this.generateTokenPair(user);
538
628
  }
539
- // Clear expired tokens from blacklist periodically
540
- cleanupBlacklist() {
541
- tokenBlacklist.clear();
542
- logger.debug("Token blacklist cleared");
629
+ async verifyPasswordById(userId, _password) {
630
+ logger.debug({ userId }, "Password verification requested");
631
+ return false;
543
632
  }
544
633
  };
545
634
  function createAuthService(app) {
@@ -666,19 +755,10 @@ var searchSchema = z3.object({
666
755
  var emailSchema = z3.string().email("Invalid email address");
667
756
  var passwordSchema = z3.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");
668
757
  var urlSchema = z3.string().url("Invalid URL format");
669
- var phoneSchema = z3.string().regex(
670
- /^\+?[1-9]\d{1,14}$/,
671
- "Invalid phone number format"
672
- );
758
+ var phoneSchema = z3.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number format");
673
759
  var dateSchema = z3.coerce.date();
674
- var futureDateSchema = z3.coerce.date().refine(
675
- (date) => date > /* @__PURE__ */ new Date(),
676
- "Date must be in the future"
677
- );
678
- var pastDateSchema = z3.coerce.date().refine(
679
- (date) => date < /* @__PURE__ */ new Date(),
680
- "Date must be in the past"
681
- );
760
+ var futureDateSchema = z3.coerce.date().refine((date) => date > /* @__PURE__ */ new Date(), "Date must be in the future");
761
+ var pastDateSchema = z3.coerce.date().refine((date) => date < /* @__PURE__ */ new Date(), "Date must be in the past");
682
762
 
683
763
  // src/modules/auth/auth.controller.ts
684
764
  var AuthController = class {
@@ -749,7 +829,7 @@ var AuthController = class {
749
829
  if (!user || user.status !== "active") {
750
830
  throw new UnauthorizedError("User not found or inactive");
751
831
  }
752
- this.authService.blacklistToken(data.refreshToken);
832
+ await this.authService.blacklistToken(data.refreshToken);
753
833
  const tokens = this.authService.generateTokenPair({
754
834
  id: user.id,
755
835
  email: user.email,
@@ -761,7 +841,7 @@ var AuthController = class {
761
841
  const authHeader = request.headers.authorization;
762
842
  if (authHeader?.startsWith("Bearer ")) {
763
843
  const token = authHeader.substring(7);
764
- this.authService.blacklistToken(token);
844
+ await this.authService.blacklistToken(token);
765
845
  }
766
846
  success(reply, { message: "Logged out successfully" });
767
847
  }
@@ -805,7 +885,7 @@ function createAuthController(authService, userService) {
805
885
 
806
886
  // src/modules/auth/auth.middleware.ts
807
887
  function createAuthMiddleware(authService) {
808
- return async function authenticate(request, reply) {
888
+ return async function authenticate(request, _reply) {
809
889
  const authHeader = request.headers.authorization;
810
890
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
811
891
  throw new UnauthorizedError("Missing or invalid authorization header");
@@ -830,7 +910,7 @@ function createRoleMiddleware(allowedRoles) {
830
910
  }
831
911
  };
832
912
  }
833
- function createPermissionMiddleware(requiredPermissions) {
913
+ function createPermissionMiddleware(_requiredPermissions) {
834
914
  return async function checkPermissions(request, _reply) {
835
915
  const user = request.user;
836
916
  if (!user) {
@@ -857,257 +937,33 @@ function createOptionalAuthMiddleware(authService) {
857
937
  };
858
938
  }
859
939
 
860
- // src/modules/swagger/swagger.service.ts
861
- import swagger from "@fastify/swagger";
862
- import swaggerUi from "@fastify/swagger-ui";
863
- var defaultConfig3 = {
864
- enabled: true,
865
- route: "/docs",
866
- title: "Servcraft API",
867
- description: "API documentation generated by Servcraft",
868
- version: "1.0.0",
869
- tags: [
870
- { name: "Auth", description: "Authentication endpoints" },
871
- { name: "Users", description: "User management endpoints" },
872
- { name: "Health", description: "Health check endpoints" }
873
- ]
874
- };
875
- async function registerSwagger(app, customConfig) {
876
- const swaggerConfig = { ...defaultConfig3, ...customConfig };
877
- if (swaggerConfig.enabled === false) {
878
- logger.info("Swagger documentation disabled");
879
- return;
880
- }
881
- await app.register(swagger, {
882
- openapi: {
883
- openapi: "3.0.3",
884
- info: {
885
- title: swaggerConfig.title,
886
- description: swaggerConfig.description,
887
- version: swaggerConfig.version,
888
- contact: swaggerConfig.contact,
889
- license: swaggerConfig.license
890
- },
891
- servers: swaggerConfig.servers || [
892
- {
893
- url: `http://localhost:${config.server.port}`,
894
- description: "Development server"
895
- }
896
- ],
897
- tags: swaggerConfig.tags,
898
- components: {
899
- securitySchemes: {
900
- bearerAuth: {
901
- type: "http",
902
- scheme: "bearer",
903
- bearerFormat: "JWT",
904
- description: "Enter your JWT token"
905
- }
906
- }
907
- }
908
- }
909
- });
910
- await app.register(swaggerUi, {
911
- routePrefix: swaggerConfig.route || "/docs",
912
- uiConfig: {
913
- docExpansion: "list",
914
- deepLinking: true,
915
- displayRequestDuration: true,
916
- filter: true,
917
- showExtensions: true,
918
- showCommonExtensions: true
919
- },
920
- staticCSP: true,
921
- transformStaticCSP: (header) => header
922
- });
923
- logger.info("Swagger documentation registered at /docs");
924
- }
925
- var commonResponses = {
926
- success: {
927
- type: "object",
928
- properties: {
929
- success: { type: "boolean", example: true },
930
- data: { type: "object" }
931
- }
932
- },
933
- error: {
934
- type: "object",
935
- properties: {
936
- success: { type: "boolean", example: false },
937
- message: { type: "string" },
938
- errors: {
939
- type: "object",
940
- additionalProperties: {
941
- type: "array",
942
- items: { type: "string" }
943
- }
944
- }
945
- }
946
- },
947
- unauthorized: {
948
- type: "object",
949
- properties: {
950
- success: { type: "boolean", example: false },
951
- message: { type: "string", example: "Unauthorized" }
952
- }
953
- },
954
- notFound: {
955
- type: "object",
956
- properties: {
957
- success: { type: "boolean", example: false },
958
- message: { type: "string", example: "Resource not found" }
959
- }
960
- },
961
- paginated: {
962
- type: "object",
963
- properties: {
964
- success: { type: "boolean", example: true },
965
- data: {
966
- type: "object",
967
- properties: {
968
- data: { type: "array", items: { type: "object" } },
969
- meta: {
970
- type: "object",
971
- properties: {
972
- total: { type: "number" },
973
- page: { type: "number" },
974
- limit: { type: "number" },
975
- totalPages: { type: "number" },
976
- hasNextPage: { type: "boolean" },
977
- hasPrevPage: { type: "boolean" }
978
- }
979
- }
980
- }
981
- }
982
- }
983
- }
984
- };
985
- var paginationQuery = {
986
- type: "object",
987
- properties: {
988
- page: { type: "integer", minimum: 1, default: 1, description: "Page number" },
989
- limit: { type: "integer", minimum: 1, maximum: 100, default: 20, description: "Items per page" },
990
- sortBy: { type: "string", description: "Field to sort by" },
991
- sortOrder: { type: "string", enum: ["asc", "desc"], default: "asc", description: "Sort order" },
992
- search: { type: "string", description: "Search query" }
993
- }
994
- };
995
- var idParam = {
996
- type: "object",
997
- properties: {
998
- id: { type: "string", format: "uuid", description: "Resource ID" }
999
- },
1000
- required: ["id"]
1001
- };
1002
-
1003
940
  // src/modules/auth/auth.routes.ts
1004
- var credentialsBody = {
1005
- type: "object",
1006
- required: ["email", "password"],
1007
- properties: {
1008
- email: { type: "string", format: "email" },
1009
- password: { type: "string", minLength: 8 }
1010
- }
1011
- };
1012
- var changePasswordBody = {
1013
- type: "object",
1014
- required: ["currentPassword", "newPassword"],
1015
- properties: {
1016
- currentPassword: { type: "string", minLength: 8 },
1017
- newPassword: { type: "string", minLength: 8 }
1018
- }
1019
- };
1020
941
  function registerAuthRoutes(app, controller, authService) {
1021
942
  const authenticate = createAuthMiddleware(authService);
1022
- app.post("/auth/register", {
1023
- schema: {
1024
- tags: ["Auth"],
1025
- summary: "Register a new user",
1026
- body: credentialsBody,
1027
- response: {
1028
- 201: commonResponses.success,
1029
- 400: commonResponses.error,
1030
- 409: commonResponses.error
1031
- }
1032
- },
1033
- handler: controller.register.bind(controller)
1034
- });
1035
- app.post("/auth/login", {
1036
- schema: {
1037
- tags: ["Auth"],
1038
- summary: "Login and obtain tokens",
1039
- body: credentialsBody,
1040
- response: {
1041
- 200: commonResponses.success,
1042
- 400: commonResponses.error,
1043
- 401: commonResponses.unauthorized
1044
- }
1045
- },
1046
- handler: controller.login.bind(controller)
1047
- });
1048
- app.post("/auth/refresh", {
1049
- schema: {
1050
- tags: ["Auth"],
1051
- summary: "Refresh access token",
1052
- body: {
1053
- type: "object",
1054
- required: ["refreshToken"],
1055
- properties: {
1056
- refreshToken: { type: "string" }
1057
- }
1058
- },
1059
- response: {
1060
- 200: commonResponses.success,
1061
- 401: commonResponses.unauthorized
1062
- }
1063
- },
1064
- handler: controller.refresh.bind(controller)
1065
- });
1066
- app.post("/auth/logout", {
1067
- preHandler: [authenticate],
1068
- schema: {
1069
- tags: ["Auth"],
1070
- summary: "Logout current user",
1071
- security: [{ bearerAuth: [] }],
1072
- response: {
1073
- 200: commonResponses.success,
1074
- 401: commonResponses.unauthorized
1075
- }
1076
- },
1077
- handler: controller.logout.bind(controller)
1078
- });
1079
- app.get("/auth/me", {
1080
- preHandler: [authenticate],
1081
- schema: {
1082
- tags: ["Auth"],
1083
- summary: "Get current user profile",
1084
- security: [{ bearerAuth: [] }],
1085
- response: {
1086
- 200: commonResponses.success,
1087
- 401: commonResponses.unauthorized
1088
- }
1089
- },
1090
- handler: controller.me.bind(controller)
1091
- });
1092
- app.post("/auth/change-password", {
1093
- preHandler: [authenticate],
1094
- schema: {
1095
- tags: ["Auth"],
1096
- summary: "Change current user password",
1097
- security: [{ bearerAuth: [] }],
1098
- body: changePasswordBody,
1099
- response: {
1100
- 200: commonResponses.success,
1101
- 400: commonResponses.error,
1102
- 401: commonResponses.unauthorized
1103
- }
1104
- },
1105
- handler: controller.changePassword.bind(controller)
1106
- });
943
+ app.post("/auth/register", controller.register.bind(controller));
944
+ app.post("/auth/login", controller.login.bind(controller));
945
+ app.post("/auth/refresh", controller.refresh.bind(controller));
946
+ app.post("/auth/logout", { preHandler: [authenticate] }, controller.logout.bind(controller));
947
+ app.get("/auth/me", { preHandler: [authenticate] }, controller.me.bind(controller));
948
+ app.post(
949
+ "/auth/change-password",
950
+ { preHandler: [authenticate] },
951
+ controller.changePassword.bind(controller)
952
+ );
1107
953
  }
1108
954
 
1109
- // src/modules/user/user.repository.ts
1110
- import { randomUUID } from "crypto";
955
+ // src/database/prisma.ts
956
+ import { PrismaClient } from "@prisma/client";
957
+ var prismaClientSingleton = () => {
958
+ return new PrismaClient({
959
+ log: isProduction() ? ["error"] : ["query", "info", "warn", "error"],
960
+ errorFormat: isProduction() ? "minimal" : "pretty"
961
+ });
962
+ };
963
+ var prisma = globalThis.__prisma ?? prismaClientSingleton();
964
+ if (!isProduction()) {
965
+ globalThis.__prisma = prisma;
966
+ }
1111
967
 
1112
968
  // src/utils/pagination.ts
1113
969
  var DEFAULT_PAGE = 1;
@@ -1115,7 +971,10 @@ var DEFAULT_LIMIT = 20;
1115
971
  var MAX_LIMIT = 100;
1116
972
  function parsePaginationParams(query) {
1117
973
  const page = Math.max(1, parseInt(String(query.page || DEFAULT_PAGE), 10));
1118
- const limit = Math.min(MAX_LIMIT, Math.max(1, parseInt(String(query.limit || DEFAULT_LIMIT), 10)));
974
+ const limit = Math.min(
975
+ MAX_LIMIT,
976
+ Math.max(1, parseInt(String(query.limit || DEFAULT_LIMIT), 10))
977
+ );
1119
978
  const sortBy = typeof query.sortBy === "string" ? query.sortBy : void 0;
1120
979
  const sortOrder = query.sortOrder === "desc" ? "desc" : "asc";
1121
980
  return { page, limit, sortBy, sortOrder };
@@ -1139,122 +998,231 @@ function getSkip(params) {
1139
998
  }
1140
999
 
1141
1000
  // src/modules/user/user.repository.ts
1142
- var users = /* @__PURE__ */ new Map();
1001
+ import { UserRole, UserStatus } from "@prisma/client";
1143
1002
  var UserRepository = class {
1003
+ /**
1004
+ * Find user by ID
1005
+ */
1144
1006
  async findById(id) {
1145
- return users.get(id) || null;
1007
+ const user = await prisma.user.findUnique({
1008
+ where: { id }
1009
+ });
1010
+ if (!user) return null;
1011
+ return this.mapPrismaUserToUser(user);
1146
1012
  }
1013
+ /**
1014
+ * Find user by email (case-insensitive)
1015
+ */
1147
1016
  async findByEmail(email) {
1148
- for (const user of users.values()) {
1149
- if (user.email.toLowerCase() === email.toLowerCase()) {
1150
- return user;
1151
- }
1152
- }
1153
- return null;
1017
+ const user = await prisma.user.findUnique({
1018
+ where: { email: email.toLowerCase() }
1019
+ });
1020
+ if (!user) return null;
1021
+ return this.mapPrismaUserToUser(user);
1154
1022
  }
1023
+ /**
1024
+ * Find multiple users with pagination and filters
1025
+ */
1155
1026
  async findMany(params, filters) {
1156
- let filteredUsers = Array.from(users.values());
1157
- if (filters) {
1158
- if (filters.status) {
1159
- filteredUsers = filteredUsers.filter((u) => u.status === filters.status);
1160
- }
1161
- if (filters.role) {
1162
- filteredUsers = filteredUsers.filter((u) => u.role === filters.role);
1163
- }
1164
- if (filters.emailVerified !== void 0) {
1165
- filteredUsers = filteredUsers.filter((u) => u.emailVerified === filters.emailVerified);
1166
- }
1167
- if (filters.search) {
1168
- const search = filters.search.toLowerCase();
1169
- filteredUsers = filteredUsers.filter(
1170
- (u) => u.email.toLowerCase().includes(search) || u.name?.toLowerCase().includes(search)
1171
- );
1172
- }
1173
- }
1174
- if (params.sortBy) {
1175
- const sortKey = params.sortBy;
1176
- filteredUsers.sort((a, b) => {
1177
- const aVal = a[sortKey];
1178
- const bVal = b[sortKey];
1179
- if (aVal === void 0 || bVal === void 0) return 0;
1180
- if (aVal < bVal) return params.sortOrder === "desc" ? 1 : -1;
1181
- if (aVal > bVal) return params.sortOrder === "desc" ? -1 : 1;
1182
- return 0;
1183
- });
1184
- }
1185
- const total = filteredUsers.length;
1186
- const skip = getSkip(params);
1187
- const data = filteredUsers.slice(skip, skip + params.limit);
1188
- return createPaginatedResult(data, total, params);
1189
- }
1027
+ const where = this.buildWhereClause(filters);
1028
+ const orderBy = this.buildOrderBy(params);
1029
+ const [data, total] = await Promise.all([
1030
+ prisma.user.findMany({
1031
+ where,
1032
+ orderBy,
1033
+ skip: getSkip(params),
1034
+ take: params.limit
1035
+ }),
1036
+ prisma.user.count({ where })
1037
+ ]);
1038
+ const mappedUsers = data.map((user) => this.mapPrismaUserToUser(user));
1039
+ return createPaginatedResult(mappedUsers, total, params);
1040
+ }
1041
+ /**
1042
+ * Create a new user
1043
+ */
1190
1044
  async create(data) {
1191
- const now = /* @__PURE__ */ new Date();
1192
- const user = {
1193
- id: randomUUID(),
1194
- email: data.email,
1195
- password: data.password,
1196
- name: data.name,
1197
- role: data.role || "user",
1198
- status: "active",
1199
- emailVerified: false,
1200
- createdAt: now,
1201
- updatedAt: now
1202
- };
1203
- users.set(user.id, user);
1204
- return user;
1045
+ const user = await prisma.user.create({
1046
+ data: {
1047
+ email: data.email.toLowerCase(),
1048
+ password: data.password,
1049
+ name: data.name,
1050
+ role: this.mapRoleToEnum(data.role || "user"),
1051
+ status: UserStatus.ACTIVE,
1052
+ emailVerified: false
1053
+ }
1054
+ });
1055
+ return this.mapPrismaUserToUser(user);
1205
1056
  }
1057
+ /**
1058
+ * Update user data
1059
+ */
1206
1060
  async update(id, data) {
1207
- const user = users.get(id);
1208
- if (!user) return null;
1209
- const updatedUser = {
1210
- ...user,
1211
- ...data,
1212
- updatedAt: /* @__PURE__ */ new Date()
1213
- };
1214
- users.set(id, updatedUser);
1215
- return updatedUser;
1061
+ try {
1062
+ const user = await prisma.user.update({
1063
+ where: { id },
1064
+ data: {
1065
+ ...data.email && { email: data.email.toLowerCase() },
1066
+ ...data.name !== void 0 && { name: data.name },
1067
+ ...data.role && { role: this.mapRoleToEnum(data.role) },
1068
+ ...data.status && { status: this.mapStatusToEnum(data.status) },
1069
+ ...data.emailVerified !== void 0 && { emailVerified: data.emailVerified },
1070
+ ...data.metadata && { metadata: data.metadata }
1071
+ }
1072
+ });
1073
+ return this.mapPrismaUserToUser(user);
1074
+ } catch {
1075
+ return null;
1076
+ }
1216
1077
  }
1078
+ /**
1079
+ * Update user password
1080
+ */
1217
1081
  async updatePassword(id, password) {
1218
- const user = users.get(id);
1219
- if (!user) return null;
1220
- const updatedUser = {
1221
- ...user,
1222
- password,
1223
- updatedAt: /* @__PURE__ */ new Date()
1224
- };
1225
- users.set(id, updatedUser);
1226
- return updatedUser;
1082
+ try {
1083
+ const user = await prisma.user.update({
1084
+ where: { id },
1085
+ data: { password }
1086
+ });
1087
+ return this.mapPrismaUserToUser(user);
1088
+ } catch {
1089
+ return null;
1090
+ }
1227
1091
  }
1092
+ /**
1093
+ * Update last login timestamp
1094
+ */
1228
1095
  async updateLastLogin(id) {
1229
- const user = users.get(id);
1230
- if (!user) return null;
1231
- const updatedUser = {
1232
- ...user,
1233
- lastLoginAt: /* @__PURE__ */ new Date(),
1234
- updatedAt: /* @__PURE__ */ new Date()
1235
- };
1236
- users.set(id, updatedUser);
1237
- return updatedUser;
1096
+ try {
1097
+ const user = await prisma.user.update({
1098
+ where: { id },
1099
+ data: { lastLoginAt: /* @__PURE__ */ new Date() }
1100
+ });
1101
+ return this.mapPrismaUserToUser(user);
1102
+ } catch {
1103
+ return null;
1104
+ }
1238
1105
  }
1106
+ /**
1107
+ * Delete user by ID
1108
+ */
1239
1109
  async delete(id) {
1240
- return users.delete(id);
1110
+ try {
1111
+ await prisma.user.delete({
1112
+ where: { id }
1113
+ });
1114
+ return true;
1115
+ } catch {
1116
+ return false;
1117
+ }
1241
1118
  }
1119
+ /**
1120
+ * Count users with optional filters
1121
+ */
1242
1122
  async count(filters) {
1243
- let count = 0;
1244
- for (const user of users.values()) {
1245
- if (filters) {
1246
- if (filters.status && user.status !== filters.status) continue;
1247
- if (filters.role && user.role !== filters.role) continue;
1248
- if (filters.emailVerified !== void 0 && user.emailVerified !== filters.emailVerified)
1249
- continue;
1123
+ const where = this.buildWhereClause(filters);
1124
+ return prisma.user.count({ where });
1125
+ }
1126
+ /**
1127
+ * Helper to clear all users (for testing only)
1128
+ * WARNING: This deletes all users from the database
1129
+ */
1130
+ async clear() {
1131
+ await prisma.user.deleteMany();
1132
+ }
1133
+ /**
1134
+ * Build Prisma where clause from filters
1135
+ */
1136
+ buildWhereClause(filters) {
1137
+ if (!filters) return {};
1138
+ return {
1139
+ ...filters.status && { status: this.mapStatusToEnum(filters.status) },
1140
+ ...filters.role && { role: this.mapRoleToEnum(filters.role) },
1141
+ ...filters.emailVerified !== void 0 && { emailVerified: filters.emailVerified },
1142
+ ...filters.search && {
1143
+ OR: [
1144
+ { email: { contains: filters.search, mode: "insensitive" } },
1145
+ { name: { contains: filters.search, mode: "insensitive" } }
1146
+ ]
1250
1147
  }
1251
- count++;
1148
+ };
1149
+ }
1150
+ /**
1151
+ * Build Prisma orderBy clause from pagination params
1152
+ */
1153
+ buildOrderBy(params) {
1154
+ if (!params.sortBy) {
1155
+ return { createdAt: "desc" };
1252
1156
  }
1253
- return count;
1157
+ return {
1158
+ [params.sortBy]: params.sortOrder || "asc"
1159
+ };
1254
1160
  }
1255
- // Helper to clear all users (for testing)
1256
- async clear() {
1257
- users.clear();
1161
+ /**
1162
+ * Map Prisma User to application User type
1163
+ */
1164
+ mapPrismaUserToUser(prismaUser) {
1165
+ return {
1166
+ id: prismaUser.id,
1167
+ email: prismaUser.email,
1168
+ password: prismaUser.password,
1169
+ name: prismaUser.name ?? void 0,
1170
+ role: this.mapEnumToRole(prismaUser.role),
1171
+ status: this.mapEnumToStatus(prismaUser.status),
1172
+ emailVerified: prismaUser.emailVerified,
1173
+ lastLoginAt: prismaUser.lastLoginAt ?? void 0,
1174
+ metadata: prismaUser.metadata,
1175
+ createdAt: prismaUser.createdAt,
1176
+ updatedAt: prismaUser.updatedAt
1177
+ };
1178
+ }
1179
+ /**
1180
+ * Map application role to Prisma enum
1181
+ */
1182
+ mapRoleToEnum(role) {
1183
+ const roleMap = {
1184
+ user: UserRole.USER,
1185
+ moderator: UserRole.MODERATOR,
1186
+ admin: UserRole.ADMIN,
1187
+ super_admin: UserRole.SUPER_ADMIN
1188
+ };
1189
+ return roleMap[role] || UserRole.USER;
1190
+ }
1191
+ /**
1192
+ * Map Prisma enum to application role
1193
+ */
1194
+ mapEnumToRole(role) {
1195
+ const roleMap = {
1196
+ [UserRole.USER]: "user",
1197
+ [UserRole.MODERATOR]: "moderator",
1198
+ [UserRole.ADMIN]: "admin",
1199
+ [UserRole.SUPER_ADMIN]: "super_admin"
1200
+ };
1201
+ return roleMap[role];
1202
+ }
1203
+ /**
1204
+ * Map application status to Prisma enum
1205
+ */
1206
+ mapStatusToEnum(status) {
1207
+ const statusMap = {
1208
+ active: UserStatus.ACTIVE,
1209
+ inactive: UserStatus.INACTIVE,
1210
+ suspended: UserStatus.SUSPENDED,
1211
+ banned: UserStatus.BANNED
1212
+ };
1213
+ return statusMap[status] || UserStatus.ACTIVE;
1214
+ }
1215
+ /**
1216
+ * Map Prisma enum to application status
1217
+ */
1218
+ mapEnumToStatus(status) {
1219
+ const statusMap = {
1220
+ [UserStatus.ACTIVE]: "active",
1221
+ [UserStatus.INACTIVE]: "inactive",
1222
+ [UserStatus.SUSPENDED]: "suspended",
1223
+ [UserStatus.BANNED]: "banned"
1224
+ };
1225
+ return statusMap[status];
1258
1226
  }
1259
1227
  };
1260
1228
  function createUserRepository() {
@@ -1300,7 +1268,7 @@ var UserService = class {
1300
1268
  const result = await this.repository.findMany(params, filters);
1301
1269
  return {
1302
1270
  ...result,
1303
- data: result.data.map(({ password, ...user }) => user)
1271
+ data: result.data.map(({ password: _password, ...user }) => user)
1304
1272
  };
1305
1273
  }
1306
1274
  async create(data) {
@@ -1377,7 +1345,7 @@ var UserService = class {
1377
1345
  if (permissions.includes(permission)) {
1378
1346
  return true;
1379
1347
  }
1380
- const [resource, action] = permission.split(":");
1348
+ const [resource] = permission.split(":");
1381
1349
  const managePermission = `${resource}:manage`;
1382
1350
  if (permissions.includes(managePermission)) {
1383
1351
  return true;
@@ -1409,7 +1377,6 @@ async function registerAuthModule(app) {
1409
1377
  const authController = createAuthController(authService, userService);
1410
1378
  registerAuthRoutes(app, authController, authService);
1411
1379
  logger.info("Auth module registered");
1412
- return authService;
1413
1380
  }
1414
1381
 
1415
1382
  // src/modules/user/schemas.ts
@@ -1446,6 +1413,11 @@ var userQuerySchema = z4.object({
1446
1413
  });
1447
1414
 
1448
1415
  // src/modules/user/user.controller.ts
1416
+ function omitPassword(user) {
1417
+ const { password, ...userData } = user;
1418
+ void password;
1419
+ return userData;
1420
+ }
1449
1421
  var UserController = class {
1450
1422
  constructor(userService) {
1451
1423
  this.userService = userService;
@@ -1470,14 +1442,12 @@ var UserController = class {
1470
1442
  message: "User not found"
1471
1443
  });
1472
1444
  }
1473
- const { password, ...userData } = user;
1474
- success(reply, userData);
1445
+ success(reply, omitPassword(user));
1475
1446
  }
1476
1447
  async update(request, reply) {
1477
1448
  const data = validateBody(updateUserSchema, request.body);
1478
1449
  const user = await this.userService.update(request.params.id, data);
1479
- const { password, ...userData } = user;
1480
- success(reply, userData);
1450
+ success(reply, omitPassword(user));
1481
1451
  }
1482
1452
  async delete(request, reply) {
1483
1453
  const authRequest = request;
@@ -1493,7 +1463,7 @@ var UserController = class {
1493
1463
  throw new ForbiddenError("Cannot suspend your own account");
1494
1464
  }
1495
1465
  const user = await this.userService.suspend(request.params.id);
1496
- const { password, ...userData } = user;
1466
+ const userData = omitPassword(user);
1497
1467
  success(reply, userData);
1498
1468
  }
1499
1469
  async ban(request, reply) {
@@ -1502,12 +1472,12 @@ var UserController = class {
1502
1472
  throw new ForbiddenError("Cannot ban your own account");
1503
1473
  }
1504
1474
  const user = await this.userService.ban(request.params.id);
1505
- const { password, ...userData } = user;
1475
+ const userData = omitPassword(user);
1506
1476
  success(reply, userData);
1507
1477
  }
1508
1478
  async activate(request, reply) {
1509
1479
  const user = await this.userService.activate(request.params.id);
1510
- const { password, ...userData } = user;
1480
+ const userData = omitPassword(user);
1511
1481
  success(reply, userData);
1512
1482
  }
1513
1483
  // Profile routes (for authenticated user)
@@ -1520,14 +1490,14 @@ var UserController = class {
1520
1490
  message: "User not found"
1521
1491
  });
1522
1492
  }
1523
- const { password, ...userData } = user;
1493
+ const userData = omitPassword(user);
1524
1494
  success(reply, userData);
1525
1495
  }
1526
1496
  async updateProfile(request, reply) {
1527
1497
  const authRequest = request;
1528
1498
  const data = validateBody(updateProfileSchema, request.body);
1529
1499
  const user = await this.userService.update(authRequest.user.id, data);
1530
- const { password, ...userData } = user;
1500
+ const userData = omitPassword(user);
1531
1501
  success(reply, userData);
1532
1502
  }
1533
1503
  };
@@ -1536,186 +1506,61 @@ function createUserController(userService) {
1536
1506
  }
1537
1507
 
1538
1508
  // src/modules/user/user.routes.ts
1539
- var userTag = "Users";
1540
- var userResponse = {
1509
+ var idParamsSchema = {
1541
1510
  type: "object",
1542
1511
  properties: {
1543
- success: { type: "boolean", example: true },
1544
- data: { type: "object" }
1545
- }
1512
+ id: { type: "string" }
1513
+ },
1514
+ required: ["id"]
1546
1515
  };
1547
1516
  function registerUserRoutes(app, controller, authService) {
1548
1517
  const authenticate = createAuthMiddleware(authService);
1549
1518
  const isAdmin = createRoleMiddleware(["admin", "super_admin"]);
1550
1519
  const isModerator = createRoleMiddleware(["moderator", "admin", "super_admin"]);
1551
- app.get(
1552
- "/profile",
1553
- {
1554
- preHandler: [authenticate],
1555
- schema: {
1556
- tags: [userTag],
1557
- summary: "Get current user profile",
1558
- security: [{ bearerAuth: [] }],
1559
- response: {
1560
- 200: userResponse,
1561
- 401: commonResponses.unauthorized
1562
- }
1563
- }
1564
- },
1565
- controller.getProfile.bind(controller)
1566
- );
1567
- app.patch(
1568
- "/profile",
1569
- {
1570
- preHandler: [authenticate],
1571
- schema: {
1572
- tags: [userTag],
1573
- summary: "Update current user profile",
1574
- security: [{ bearerAuth: [] }],
1575
- body: { type: "object" },
1576
- response: {
1577
- 200: userResponse,
1578
- 401: commonResponses.unauthorized,
1579
- 400: commonResponses.error
1580
- }
1581
- }
1582
- },
1583
- controller.updateProfile.bind(controller)
1584
- );
1585
- app.get(
1586
- "/users",
1587
- {
1588
- preHandler: [authenticate, isModerator],
1589
- schema: {
1590
- tags: [userTag],
1591
- summary: "List users",
1592
- security: [{ bearerAuth: [] }],
1593
- querystring: {
1594
- ...paginationQuery,
1595
- properties: {
1596
- ...paginationQuery.properties,
1597
- status: { type: "string", enum: ["active", "inactive", "suspended", "banned"] },
1598
- role: { type: "string", enum: ["user", "admin", "moderator", "super_admin"] },
1599
- search: { type: "string" },
1600
- emailVerified: { type: "boolean" }
1601
- }
1602
- },
1603
- response: {
1604
- 200: commonResponses.paginated,
1605
- 401: commonResponses.unauthorized
1606
- }
1607
- }
1608
- },
1609
- controller.list.bind(controller)
1610
- );
1520
+ app.get("/profile", { preHandler: [authenticate] }, controller.getProfile.bind(controller));
1521
+ app.patch("/profile", { preHandler: [authenticate] }, controller.updateProfile.bind(controller));
1522
+ app.get("/users", { preHandler: [authenticate, isModerator] }, controller.list.bind(controller));
1611
1523
  app.get(
1612
1524
  "/users/:id",
1613
- {
1614
- preHandler: [authenticate, isModerator],
1615
- schema: {
1616
- tags: [userTag],
1617
- summary: "Get user by id",
1618
- security: [{ bearerAuth: [] }],
1619
- params: idParam,
1620
- response: {
1621
- 200: userResponse,
1622
- 401: commonResponses.unauthorized,
1623
- 404: commonResponses.notFound
1624
- }
1625
- }
1626
- },
1627
- controller.getById.bind(controller)
1525
+ { preHandler: [authenticate, isModerator], schema: { params: idParamsSchema } },
1526
+ async (request, reply) => {
1527
+ return controller.getById(request, reply);
1528
+ }
1628
1529
  );
1629
1530
  app.patch(
1630
1531
  "/users/:id",
1631
- {
1632
- preHandler: [authenticate, isAdmin],
1633
- schema: {
1634
- tags: [userTag],
1635
- summary: "Update user",
1636
- security: [{ bearerAuth: [] }],
1637
- params: idParam,
1638
- body: { type: "object" },
1639
- response: {
1640
- 200: userResponse,
1641
- 401: commonResponses.unauthorized,
1642
- 404: commonResponses.notFound
1643
- }
1644
- }
1645
- },
1646
- controller.update.bind(controller)
1532
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
1533
+ async (request, reply) => {
1534
+ return controller.update(request, reply);
1535
+ }
1647
1536
  );
1648
1537
  app.delete(
1649
1538
  "/users/:id",
1650
- {
1651
- preHandler: [authenticate, isAdmin],
1652
- schema: {
1653
- tags: [userTag],
1654
- summary: "Delete user",
1655
- security: [{ bearerAuth: [] }],
1656
- params: idParam,
1657
- response: {
1658
- 204: { description: "User deleted" },
1659
- 401: commonResponses.unauthorized,
1660
- 404: commonResponses.notFound
1661
- }
1662
- }
1663
- },
1664
- controller.delete.bind(controller)
1539
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
1540
+ async (request, reply) => {
1541
+ return controller.delete(request, reply);
1542
+ }
1665
1543
  );
1666
1544
  app.post(
1667
1545
  "/users/:id/suspend",
1668
- {
1669
- preHandler: [authenticate, isAdmin],
1670
- schema: {
1671
- tags: [userTag],
1672
- summary: "Suspend user",
1673
- security: [{ bearerAuth: [] }],
1674
- params: idParam,
1675
- response: {
1676
- 200: userResponse,
1677
- 401: commonResponses.unauthorized,
1678
- 404: commonResponses.notFound
1679
- }
1680
- }
1681
- },
1682
- controller.suspend.bind(controller)
1546
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
1547
+ async (request, reply) => {
1548
+ return controller.suspend(request, reply);
1549
+ }
1683
1550
  );
1684
1551
  app.post(
1685
1552
  "/users/:id/ban",
1686
- {
1687
- preHandler: [authenticate, isAdmin],
1688
- schema: {
1689
- tags: [userTag],
1690
- summary: "Ban user",
1691
- security: [{ bearerAuth: [] }],
1692
- params: idParam,
1693
- response: {
1694
- 200: userResponse,
1695
- 401: commonResponses.unauthorized,
1696
- 404: commonResponses.notFound
1697
- }
1698
- }
1699
- },
1700
- controller.ban.bind(controller)
1553
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
1554
+ async (request, reply) => {
1555
+ return controller.ban(request, reply);
1556
+ }
1701
1557
  );
1702
1558
  app.post(
1703
1559
  "/users/:id/activate",
1704
- {
1705
- preHandler: [authenticate, isAdmin],
1706
- schema: {
1707
- tags: [userTag],
1708
- summary: "Activate user",
1709
- security: [{ bearerAuth: [] }],
1710
- params: idParam,
1711
- response: {
1712
- 200: userResponse,
1713
- 401: commonResponses.unauthorized,
1714
- 404: commonResponses.notFound
1715
- }
1716
- }
1717
- },
1718
- controller.activate.bind(controller)
1560
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
1561
+ async (request, reply) => {
1562
+ return controller.activate(request, reply);
1563
+ }
1719
1564
  );
1720
1565
  }
1721
1566
 
@@ -1975,10 +1820,7 @@ var EmailService = class {
1975
1820
  return { success: true, messageId: "dev-mode" };
1976
1821
  }
1977
1822
  const result = await this.transporter.sendMail(mailOptions);
1978
- logger.info(
1979
- { messageId: result.messageId, to: options.to },
1980
- "Email sent successfully"
1981
- );
1823
+ logger.info({ messageId: result.messageId, to: options.to }, "Email sent successfully");
1982
1824
  return {
1983
1825
  success: true,
1984
1826
  messageId: result.messageId
@@ -2078,18 +1920,178 @@ function createEmailService(config2) {
2078
1920
  return new EmailService(config2);
2079
1921
  }
2080
1922
 
1923
+ // src/modules/audit/audit.repository.ts
1924
+ var AuditRepository = class {
1925
+ constructor(prisma2) {
1926
+ this.prisma = prisma2;
1927
+ }
1928
+ /**
1929
+ * Create a new audit log entry
1930
+ */
1931
+ async create(entry) {
1932
+ const log = await this.prisma.auditLog.create({
1933
+ data: {
1934
+ userId: entry.userId,
1935
+ action: entry.action,
1936
+ resource: entry.resource,
1937
+ resourceId: entry.resourceId,
1938
+ oldValue: entry.oldValue,
1939
+ newValue: entry.newValue,
1940
+ ipAddress: entry.ipAddress,
1941
+ userAgent: entry.userAgent,
1942
+ metadata: entry.metadata
1943
+ }
1944
+ });
1945
+ return this.mapFromPrisma(log);
1946
+ }
1947
+ /**
1948
+ * Create multiple audit log entries
1949
+ */
1950
+ async createMany(entries) {
1951
+ const result = await this.prisma.auditLog.createMany({
1952
+ data: entries.map((entry) => ({
1953
+ userId: entry.userId,
1954
+ action: entry.action,
1955
+ resource: entry.resource,
1956
+ resourceId: entry.resourceId,
1957
+ oldValue: entry.oldValue,
1958
+ newValue: entry.newValue,
1959
+ ipAddress: entry.ipAddress,
1960
+ userAgent: entry.userAgent,
1961
+ metadata: entry.metadata
1962
+ }))
1963
+ });
1964
+ return result.count;
1965
+ }
1966
+ /**
1967
+ * Find audit log by ID
1968
+ */
1969
+ async findById(id) {
1970
+ const log = await this.prisma.auditLog.findUnique({
1971
+ where: { id }
1972
+ });
1973
+ return log ? this.mapFromPrisma(log) : null;
1974
+ }
1975
+ /**
1976
+ * Query audit logs with filters and pagination
1977
+ */
1978
+ async query(params) {
1979
+ const { page = 1, limit = 20 } = params;
1980
+ const pagination = { page, limit };
1981
+ const where = {};
1982
+ if (params.userId) where.userId = params.userId;
1983
+ if (params.action) where.action = params.action;
1984
+ if (params.resource) where.resource = params.resource;
1985
+ if (params.resourceId) where.resourceId = params.resourceId;
1986
+ if (params.startDate || params.endDate) {
1987
+ where.createdAt = {};
1988
+ if (params.startDate) where.createdAt.gte = params.startDate;
1989
+ if (params.endDate) where.createdAt.lte = params.endDate;
1990
+ }
1991
+ const [logs, total] = await Promise.all([
1992
+ this.prisma.auditLog.findMany({
1993
+ where,
1994
+ orderBy: { createdAt: "desc" },
1995
+ skip: getSkip(pagination),
1996
+ take: limit
1997
+ }),
1998
+ this.prisma.auditLog.count({ where })
1999
+ ]);
2000
+ const data = logs.map((log) => this.mapFromPrisma(log));
2001
+ return createPaginatedResult(data, total, pagination);
2002
+ }
2003
+ /**
2004
+ * Find logs by user ID
2005
+ */
2006
+ async findByUser(userId, limit = 50) {
2007
+ const logs = await this.prisma.auditLog.findMany({
2008
+ where: { userId },
2009
+ orderBy: { createdAt: "desc" },
2010
+ take: limit
2011
+ });
2012
+ return logs.map((log) => this.mapFromPrisma(log));
2013
+ }
2014
+ /**
2015
+ * Find logs by resource
2016
+ */
2017
+ async findByResource(resource, resourceId, limit = 50) {
2018
+ const where = { resource };
2019
+ if (resourceId) where.resourceId = resourceId;
2020
+ const logs = await this.prisma.auditLog.findMany({
2021
+ where,
2022
+ orderBy: { createdAt: "desc" },
2023
+ take: limit
2024
+ });
2025
+ return logs.map((log) => this.mapFromPrisma(log));
2026
+ }
2027
+ /**
2028
+ * Find logs by action
2029
+ */
2030
+ async findByAction(action, limit = 50) {
2031
+ const logs = await this.prisma.auditLog.findMany({
2032
+ where: { action },
2033
+ orderBy: { createdAt: "desc" },
2034
+ take: limit
2035
+ });
2036
+ return logs.map((log) => this.mapFromPrisma(log));
2037
+ }
2038
+ /**
2039
+ * Count logs with optional filters
2040
+ */
2041
+ async count(filters) {
2042
+ const where = {};
2043
+ if (filters?.userId) where.userId = filters.userId;
2044
+ if (filters?.action) where.action = filters.action;
2045
+ if (filters?.resource) where.resource = filters.resource;
2046
+ return this.prisma.auditLog.count({ where });
2047
+ }
2048
+ /**
2049
+ * Delete old audit logs (for data retention)
2050
+ */
2051
+ async deleteOlderThan(days) {
2052
+ const cutoffDate = /* @__PURE__ */ new Date();
2053
+ cutoffDate.setDate(cutoffDate.getDate() - days);
2054
+ const result = await this.prisma.auditLog.deleteMany({
2055
+ where: {
2056
+ createdAt: { lt: cutoffDate }
2057
+ }
2058
+ });
2059
+ return result.count;
2060
+ }
2061
+ /**
2062
+ * Clear all audit logs (for testing)
2063
+ */
2064
+ async clear() {
2065
+ await this.prisma.auditLog.deleteMany();
2066
+ }
2067
+ /**
2068
+ * Map Prisma model to domain type
2069
+ */
2070
+ mapFromPrisma(log) {
2071
+ return {
2072
+ id: log.id,
2073
+ userId: log.userId ?? void 0,
2074
+ action: log.action,
2075
+ resource: log.resource,
2076
+ resourceId: log.resourceId ?? void 0,
2077
+ oldValue: log.oldValue,
2078
+ newValue: log.newValue,
2079
+ ipAddress: log.ipAddress ?? void 0,
2080
+ userAgent: log.userAgent ?? void 0,
2081
+ metadata: log.metadata,
2082
+ createdAt: log.createdAt
2083
+ };
2084
+ }
2085
+ };
2086
+
2081
2087
  // src/modules/audit/audit.service.ts
2082
- import { randomUUID as randomUUID2 } from "crypto";
2083
- var auditLogs = /* @__PURE__ */ new Map();
2084
2088
  var AuditService = class {
2089
+ repository;
2090
+ constructor() {
2091
+ this.repository = new AuditRepository(prisma);
2092
+ }
2085
2093
  async log(entry) {
2086
- const id = randomUUID2();
2087
- const auditEntry = {
2088
- ...entry,
2089
- id,
2090
- createdAt: /* @__PURE__ */ new Date()
2091
- };
2092
- auditLogs.set(id, auditEntry);
2094
+ await this.repository.create(entry);
2093
2095
  logger.info(
2094
2096
  {
2095
2097
  audit: true,
@@ -2103,39 +2105,13 @@ var AuditService = class {
2103
2105
  );
2104
2106
  }
2105
2107
  async query(params) {
2106
- const { page = 1, limit = 20 } = params;
2107
- let logs = Array.from(auditLogs.values());
2108
- if (params.userId) {
2109
- logs = logs.filter((log) => log.userId === params.userId);
2110
- }
2111
- if (params.action) {
2112
- logs = logs.filter((log) => log.action === params.action);
2113
- }
2114
- if (params.resource) {
2115
- logs = logs.filter((log) => log.resource === params.resource);
2116
- }
2117
- if (params.resourceId) {
2118
- logs = logs.filter((log) => log.resourceId === params.resourceId);
2119
- }
2120
- if (params.startDate) {
2121
- logs = logs.filter((log) => log.createdAt >= params.startDate);
2122
- }
2123
- if (params.endDate) {
2124
- logs = logs.filter((log) => log.createdAt <= params.endDate);
2125
- }
2126
- logs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
2127
- const total = logs.length;
2128
- const skip = (page - 1) * limit;
2129
- const data = logs.slice(skip, skip + limit);
2130
- return createPaginatedResult(data, total, { page, limit });
2108
+ return this.repository.query(params);
2131
2109
  }
2132
2110
  async findByUser(userId, limit = 50) {
2133
- const result = await this.query({ userId, limit });
2134
- return result.data;
2111
+ return this.repository.findByUser(userId, limit);
2135
2112
  }
2136
2113
  async findByResource(resource, resourceId, limit = 50) {
2137
- const result = await this.query({ resource, resourceId, limit });
2138
- return result.data;
2114
+ return this.repository.findByResource(resource, resourceId, limit);
2139
2115
  }
2140
2116
  // Shortcut methods for common audit events
2141
2117
  async logCreate(resource, resourceId, userId, newValue, meta) {
@@ -2193,9 +2169,17 @@ var AuditService = class {
2193
2169
  ...meta
2194
2170
  });
2195
2171
  }
2172
+ // Data retention: delete old logs
2173
+ async cleanupOldLogs(retentionDays) {
2174
+ const count = await this.repository.deleteOlderThan(retentionDays);
2175
+ if (count > 0) {
2176
+ logger.info({ count, retentionDays }, "Cleaned up old audit logs");
2177
+ }
2178
+ return count;
2179
+ }
2196
2180
  // Clear all logs (for testing)
2197
2181
  async clear() {
2198
- auditLogs.clear();
2182
+ await this.repository.clear();
2199
2183
  }
2200
2184
  };
2201
2185
  var auditService = null;
@@ -2218,20 +2202,15 @@ async function bootstrap() {
2218
2202
  const app = server.instance;
2219
2203
  registerErrorHandler(app);
2220
2204
  await registerSecurity(app);
2221
- await registerSwagger(app, {
2222
- enabled: config.swagger.enabled,
2223
- route: config.swagger.route,
2224
- title: config.swagger.title,
2225
- description: config.swagger.description,
2226
- version: config.swagger.version
2227
- });
2228
- const authService = await registerAuthModule(app);
2229
- await registerUserModule(app, authService);
2205
+ await registerAuthModule(app);
2230
2206
  await server.start();
2231
- logger.info({
2232
- env: config.env.NODE_ENV,
2233
- port: config.server.port
2234
- }, "Servcraft server started");
2207
+ logger.info(
2208
+ {
2209
+ env: config.env.NODE_ENV,
2210
+ port: config.server.port
2211
+ },
2212
+ "Servcraft server started"
2213
+ );
2235
2214
  }
2236
2215
  bootstrap().catch((err) => {
2237
2216
  logger.error({ err }, "Failed to start server");