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.
- package/.claude/settings.local.json +30 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/LICENSE +21 -0
- package/README.md +1102 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- 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
|
|
566
|
+
var import_ioredis = require("ioredis");
|
|
573
567
|
var AuthService = class {
|
|
574
568
|
app;
|
|
575
569
|
SALT_ROUNDS = 12;
|
|
576
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
662
|
-
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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/
|
|
1235
|
-
var
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
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
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
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
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
-
|
|
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
|
|
1282
|
+
return {
|
|
1283
|
+
[params.sortBy]: params.sortOrder || "asc"
|
|
1284
|
+
};
|
|
1379
1285
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1665
|
-
var userResponse = {
|
|
1634
|
+
var idParamsSchema = {
|
|
1666
1635
|
type: "object",
|
|
1667
1636
|
properties: {
|
|
1668
|
-
|
|
1669
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
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
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
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
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
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
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2259
|
-
return result.data;
|
|
2236
|
+
return this.repository.findByUser(userId, limit);
|
|
2260
2237
|
}
|
|
2261
2238
|
async findByResource(resource, resourceId, limit = 50) {
|
|
2262
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
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");
|