servcraft 0.1.0 → 0.1.1
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 +29 -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/README.md +1070 -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.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
|
-
|
|
441
|
+
import { Redis } from "ioredis";
|
|
448
442
|
var AuthService = class {
|
|
449
443
|
app;
|
|
450
444
|
SALT_ROUNDS = 12;
|
|
451
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
537
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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/
|
|
1110
|
-
import {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
|
|
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
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
-
|
|
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
|
|
1157
|
+
return {
|
|
1158
|
+
[params.sortBy]: params.sortOrder || "asc"
|
|
1159
|
+
};
|
|
1254
1160
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1540
|
-
var userResponse = {
|
|
1509
|
+
var idParamsSchema = {
|
|
1541
1510
|
type: "object",
|
|
1542
1511
|
properties: {
|
|
1543
|
-
|
|
1544
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
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
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
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
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
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
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
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
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2134
|
-
return result.data;
|
|
2111
|
+
return this.repository.findByUser(userId, limit);
|
|
2135
2112
|
}
|
|
2136
2113
|
async findByResource(resource, resourceId, limit = 50) {
|
|
2137
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
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");
|